From de0d548b7ad869006651a1f44ad0766268637c1f Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Mon, 3 Nov 2025 08:26:00 +0900 Subject: [PATCH] docs: Add comprehensive technical interview guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create TECHNICAL_INTERVIEW.md with 20 technical questions - Cover Backend (5), Frontend (4), DevOps (6), Data/API (3), Problem Solving (2) - Include detailed answers with code examples - Use Obsidian-compatible callout format for collapsible answers - Add evaluation criteria (Junior/Mid/Senior levels) - Include practical coding challenge (Comments service) Technical areas covered: - API Gateway vs Service Mesh architecture - FastAPI async/await and Motor vs PyMongo - Microservice communication (REST, Pub/Sub, gRPC) - Database strategies and JWT security - React 18 features and TypeScript integration - Docker multi-stage builds and K8s deployment strategies - Health checks, monitoring, and logging - RESTful API design and MongoDB schema modeling - Traffic handling and failure scenarios fix: Update Services.tsx with TypeScript fixes - Fix ServiceType enum import (use value import, not type-only) - Fix API method name: checkHealthAll โ†’ checkAllHealth - Ensure proper enum usage in form data ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/TECHNICAL_INTERVIEW.md | 769 ++++++++++++++++++ .../console/frontend/src/pages/Services.tsx | 428 ++++++++-- 2 files changed, 1134 insertions(+), 63 deletions(-) create mode 100644 docs/TECHNICAL_INTERVIEW.md diff --git a/docs/TECHNICAL_INTERVIEW.md b/docs/TECHNICAL_INTERVIEW.md new file mode 100644 index 0000000..5141ba4 --- /dev/null +++ b/docs/TECHNICAL_INTERVIEW.md @@ -0,0 +1,769 @@ +# Site11 ํ”„๋กœ์ ํŠธ ๊ธฐ์ˆ  ๋ฉด์ ‘ ๊ฐ€์ด๋“œ + +## ํ”„๋กœ์ ํŠธ ๊ฐœ์š” +- **์•„ํ‚คํ…์ฒ˜**: API Gateway ํŒจํ„ด ๊ธฐ๋ฐ˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค +- **๊ธฐ์ˆ  ์Šคํƒ**: FastAPI, React 18, TypeScript, MongoDB, Redis, Docker, Kubernetes +- **๋„๋ฉ”์ธ**: ๋‰ด์Šค/๋ฏธ๋””์–ด ํ”Œ๋žซํผ (๋‹ค๊ตญ๊ฐ€/๋‹ค์–ธ์–ด ์ง€์›) + +--- + +## 1. ๋ฐฑ์—”๋“œ ์•„ํ‚คํ…์ฒ˜ (5๋ฌธํ•ญ) + +### Q1. API Gateway vs Service Mesh + +**์งˆ๋ฌธ**: Console์ด API Gateway ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. Service Mesh(Istio)์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ ์žฅ๋‹จ์ ์€? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **API Gateway ํŒจํ„ด (ํ˜„์žฌ)**: +> - โœ… ์ค‘์•™ํ™”๋œ ์ธ์ฆ/๋ผ์šฐํŒ…, ๊ตฌ์ถ• ๊ฐ„๋‹จ, ๋‹จ์ผ ์ง„์ž…์  +> - โŒ SPOF ๊ฐ€๋Šฅ์„ฑ, ๋ณ‘๋ชฉ ์œ„ํ—˜, Gateway ๋ณ€๊ฒฝ ์‹œ ์ „์ฒด ์˜ํ–ฅ +> +> **Service Mesh (Istio)**: +> - โœ… ์„œ๋น„์Šค ๊ฐ„ ์ง์ ‘ ํ†ต์‹ (๋‚ฎ์€ ์ง€์—ฐ), mTLS ์ž๋™, ์„ธ๋ฐ€ํ•œ ํŠธ๋ž˜ํ”ฝ ์ œ์–ด +> - โŒ ํ•™์Šต ๊ณก์„ , ๋ฆฌ์†Œ์Šค ์˜ค๋ฒ„ํ—ค๋“œ(Sidecar), ๋ณต์žกํ•œ ๋””๋ฒ„๊น… +> +> **์„ ํƒ ๊ธฐ์ค€**: +> - 30๊ฐœ ์ดํ•˜ ์„œ๋น„์Šค โ†’ API Gateway +> - 50๊ฐœ ์ด์ƒ, ๋ณต์žกํ•œ ํ†ต์‹  ํŒจํ„ด โ†’ Service Mesh + +--- + +### Q2. FastAPI ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + +**์งˆ๋ฌธ**: `async/await` ์‚ฌ์šฉ ์‹œ๊ธฐ์™€ Motor vs PyMongo ์„ ํƒ ์ด์œ ๋Š”? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **๋™์ž‘ ์ฐจ์ด**: +> ```python +> # Sync: ์š”์ฒญ 1(50ms) โ†’ ์š”์ฒญ 2(50ms) = ์ด 100ms +> # Async: ์š”์ฒญ 1 & ์š”์ฒญ 2 ๋ณ‘ํ–‰ ์ฒ˜๋ฆฌ = ์ด ~50ms +> ``` +> +> **Motor (Async) ์ถ”์ฒœ**: +> - I/O bound ์ž‘์—…(DB, API ํ˜ธ์ถœ)์— ์ ํ•ฉ +> - ๋™์‹œ ์š”์ฒญ ์‹œ ์ฒ˜๋ฆฌ๋Ÿ‰ ์ฆ๊ฐ€ +> - FastAPI์˜ ๋น„๋™๊ธฐ ํŠน์„ฑ๊ณผ ์™„๋ฒฝ ํ˜ธํ™˜ +> +> **PyMongo (Sync) ์‚ฌ์šฉ**: +> - CPU bound ์ž‘์—…(์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ, ๋ฐ์ดํ„ฐ ๋ถ„์„) +> - Sync ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ์‹œ +> +> **์ฃผ์˜**: `time.sleep()`์€ ์ „์ฒด ์ด๋ฒคํŠธ ๋ฃจํ”„ ๋ธ”๋กœํ‚น โ†’ `asyncio.sleep()` ์‚ฌ์šฉ + +--- + +### Q3. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  + +**์งˆ๋ฌธ**: REST API, Redis Pub/Sub, gRPC ๊ฐ๊ฐ ์–ธ์ œ ์‚ฌ์šฉ? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> | ๋ฐฉ์‹ | ์‚ฌ์šฉ ์‹œ๊ธฐ | ํŠน์ง• | +> |------|----------|------| +> | **REST** | ์ฆ‰์‹œ ์‘๋‹ต ํ•„์š”, ๋ฐ์ดํ„ฐ ์กฐํšŒ | Synchronous, ๊ตฌํ˜„ ๊ฐ„๋‹จ | +> | **Pub/Sub** | ์ด๋ฒคํŠธ ์•Œ๋ฆผ, ์—ฌ๋Ÿฌ ์„œ๋น„์Šค ๋ฐ˜์‘ | Asynchronous, Loose coupling | +> | **gRPC** | ๋‚ด๋ถ€ ์„œ๋น„์Šค ํ†ต์‹ , ๊ณ ์„ฑ๋Šฅ | HTTP/2, Protobuf, ํƒ€์ž… ์•ˆ์ •์„ฑ | +> +> **์˜ˆ์‹œ**: +> - ์‚ฌ์šฉ์ž ์กฐํšŒ โ†’ REST (์ฆ‰์‹œ ์‘๋‹ต) +> - ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์•Œ๋ฆผ โ†’ Pub/Sub (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) +> - ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ฐ„ ๋‚ด๋ถ€ ํ˜ธ์ถœ โ†’ gRPC (์„ฑ๋Šฅ) + +--- + +### Q4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ „๋žต + +**์งˆ๋ฌธ**: Shared MongoDB Instance vs Separate Instances ์žฅ๋‹จ์ ? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **ํ˜„์žฌ ์ „๋žต (Shared Instance, Separate DBs)**: +> ``` +> MongoDB (site11-mongodb:27017) +> โ”œโ”€โ”€ console_db +> โ”œโ”€โ”€ users_db +> โ””โ”€โ”€ news_api_db +> ``` +> +> **์žฅ์ **: ์šด์˜ ๋‹จ์ˆœ, ๋ฆฌ์†Œ์Šค ํšจ์œจ, ๋ฐฑ์—… ๊ฐ„ํŽธ, ๋น„์šฉ ์ ˆ๊ฐ +> **๋‹จ์ **: ๊ฒฉ๋ฆฌ ๋ถ€์กฑ, ํ™•์žฅ์„ฑ ์ œํ•œ, ์žฅ์•  ์ „ํŒŒ, ๋ฆฌ์†Œ์Šค ๊ฒฝํ•ฉ +> +> **Separate Instances**: +> - ์žฅ์ : ์™„์ „ ๊ฒฉ๋ฆฌ, ๋…๋ฆฝ ํ™•์žฅ, ์žฅ์•  ๊ฒฉ๋ฆฌ +> - ๋‹จ์ : ์šด์˜ ๋ณต์žก, ๋น„์šฉ ์ฆ๊ฐ€, ํŠธ๋žœ์žญ์…˜ ๋ถˆ๊ฐ€ +> +> **์„œ๋น„์Šค ๊ฐ„ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ**: +> - โŒ ์ง์ ‘ DB ์ ‘๊ทผ ๊ธˆ์ง€ +> - โœ… API ํ˜ธ์ถœ ๋˜๋Š” Data Duplication (๋น„์ •๊ทœํ™”) +> - โœ… Event-driven ๋™๊ธฐํ™” + +--- + +### Q5. JWT ์ธ์ฆ ๋ฐ ๋ณด์•ˆ + +**์งˆ๋ฌธ**: Access Token vs Refresh Token ์ฐจ์ด์™€ ํƒˆ์ทจ ๋Œ€์‘ ๋ฐฉ์•ˆ? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> | ๊ตฌ๋ถ„ | Access Token | Refresh Token | +> |------|--------------|---------------| +> | ๋ชฉ์  | API ์ ‘๊ทผ ๊ถŒํ•œ | Access Token ์žฌ๋ฐœ๊ธ‰ | +> | ๋งŒ๋ฃŒ | ์งง์Œ (15๋ถ„-1์‹œ๊ฐ„) | ๊ธธ์Œ (7์ผ-30์ผ) | +> | ์ €์žฅ | ๋ฉ”๋ชจ๋ฆฌ | HttpOnly Cookie | +> | ํƒˆ์ทจ ์‹œ | ์ œํ•œ์  ํ”ผํ•ด | ์‹ฌ๊ฐํ•œ ํ”ผํ•ด | +> +> **ํƒˆ์ทจ ๋Œ€์‘**: +> 1. **Refresh Token Rotation**: ์žฌ๋ฐœ๊ธ‰ ์‹œ ์ƒˆ๋กœ์šด ํ† ํฐ ์Œ ์ƒ์„ฑ +> 2. **Blacklist**: Redis์— ๋กœ๊ทธ์•„์›ƒ๋œ ํ† ํฐ ์ €์žฅ +> 3. **Device Binding**: ๋””๋ฐ”์ด์Šค ID๋กœ ์ œํ•œ +> 4. **IP/User-Agent ๊ฒ€์ฆ**: ๋น„์ •์ƒ ์ ‘๊ทผ ํƒ์ง€ +> +> **์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๋ณด์•ˆ**: +> - Service Token (API Key) +> - mTLS (Production) +> - Network Policy (Kubernetes) + +--- + +## 2. ํ”„๋ก ํŠธ์—”๋“œ (4๋ฌธํ•ญ) + +### Q6. React 18 ์ฃผ์š” ๋ณ€ํ™” + +**์งˆ๋ฌธ**: Concurrent Rendering๊ณผ Automatic Batching ์„ค๋ช…? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **1. Concurrent Rendering**: +> ```tsx +> const [query, setQuery] = useState(''); +> const [isPending, startTransition] = useTransition(); +> +> // ๊ธด๊ธ‰ ์—…๋ฐ์ดํŠธ (์‚ฌ์šฉ์ž ์ž…๋ ฅ) +> setQuery(e.target.value); +> +> // ๋น„๊ธด๊ธ‰ ์—…๋ฐ์ดํŠธ (๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ) - ์ค‘๋‹จ ๊ฐ€๋Šฅ +> startTransition(() => { +> fetchSearchResults(e.target.value); +> }); +> ``` +> โ†’ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ํ•ญ์ƒ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์œ ์ง€ +> +> **2. Automatic Batching**: +> ```tsx +> // React 17: fetch ์ฝœ๋ฐฑ์—์„œ 2๋ฒˆ ๋ฆฌ๋ Œ๋”๋ง +> fetch('/api').then(() => { +> setCount(c => c + 1); // ๋ฆฌ๋ Œ๋”๋ง 1 +> setFlag(f => !f); // ๋ฆฌ๋ Œ๋”๋ง 2 +> }); +> +> // React 18: ์ž๋™ ๋ฐฐ์นญ์œผ๋กœ 1๋ฒˆ๋งŒ ๋ฆฌ๋ Œ๋”๋ง +> ``` +> +> **๊ธฐํƒ€**: `Suspense`, `useDeferredValue`, `useId` + +--- + +### Q7. TypeScript ํ™œ์šฉ + +**์งˆ๋ฌธ**: Backend API ํƒ€์ž…์„ Frontend์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **๋ฐฉ๋ฒ• 1: OpenAPI ์ฝ”๋“œ ์ƒ์„ฑ** (์ถ”์ฒœ) +> ```bash +> npm install openapi-typescript-codegen +> openapi --input http://localhost:8000/openapi.json --output ./src/api/generated +> ``` +> +> ```typescript +> // ์ž๋™ ์ƒ์„ฑ๋œ ํƒ€์ž… ์‚ฌ์šฉ +> import { ArticlesService, Article } from '@/api/generated'; +> +> const articles = await ArticlesService.getArticles({ +> category: 'tech', // โœ… ํƒ€์ž… ์ฒดํฌ +> limit: 10 +> }); +> ``` +> +> **๋ฐฉ๋ฒ• 2: tRPC** (TypeScript ํ’€์Šคํƒ) +> ```typescript +> // Backend +> export const appRouter = t.router({ +> articles: { +> list: t.procedure.input(z.object({...})).query(...) +> } +> }); +> +> // Frontend - End-to-end ํƒ€์ž… ์•ˆ์ •์„ฑ +> const { data } = trpc.articles.list.useQuery({ category: 'tech' }); +> ``` +> +> **๋ฐฉ๋ฒ• 3: ์ˆ˜๋™ ํƒ€์ž… ์ •์˜** (์ž‘์€ ํ”„๋กœ์ ํŠธ) + +--- + +### Q8. ์ƒํƒœ ๊ด€๋ฆฌ + +**์งˆ๋ฌธ**: Context API, Redux, Zustand, React Query ๊ฐ๊ฐ ์–ธ์ œ ์‚ฌ์šฉ? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> | ๋„๊ตฌ | ์‚ฌ์šฉ ์‹œ๊ธฐ | ํŠน์ง• | +> |------|----------|------| +> | **Context API** | ์ „์—ญ ํ…Œ๋งˆ, ์ธ์ฆ ์ƒํƒœ | ๋‚ด์žฅ, ๋ฆฌ๋ Œ๋”๋ง ์ฃผ์˜ | +> | **Redux** | ๋ณต์žกํ•œ ์ƒํƒœ, Time-travel | Boilerplate ๋งŽ์Œ, DevTools | +> | **Zustand** | ๊ฐ„๋‹จํ•œ ์ „์—ญ ์ƒํƒœ | ๊ฒฝ๋Ÿ‰, ๊ฐ„๊ฒฐ, ๋ฆฌ๋ Œ๋”๋ง ์ตœ์ ํ™” | +> | **React Query** | ์„œ๋ฒ„ ์ƒํƒœ | ์บ์‹ฑ, ๋ฆฌํŽ˜์นญ, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ | +> +> **ํ•ต์‹ฌ**: ์ „์—ญ ์ƒํƒœ vs ์„œ๋ฒ„ ์ƒํƒœ ๊ตฌ๋ถ„ +> - ์ „์—ญ UI ์ƒํƒœ โ†’ Zustand/Redux +> - ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ โ†’ React Query + +--- + +### Q9. Material-UI ์ตœ์ ํ™” + +**์งˆ๋ฌธ**: ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”์™€ ํ…Œ๋งˆ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๋ฐฉ๋ฒ•? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **๋ฒˆ๋“ค ์ตœ์ ํ™”**: +> ```tsx +> // โŒ ์ „์ฒด import +> import { Button, TextField } from '@mui/material'; +> +> // โœ… Tree shaking +> import Button from '@mui/material/Button'; +> import TextField from '@mui/material/TextField'; +> ``` +> +> **Code Splitting**: +> ```tsx +> const Dashboard = lazy(() => import('./pages/Dashboard')); +> +> }> +> +> +> ``` +> +> **ํ…Œ๋งˆ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•**: +> ```tsx +> import { createTheme, ThemeProvider } from '@mui/material/styles'; +> +> const theme = createTheme({ +> palette: { +> mode: 'dark', +> primary: { main: '#1976d2' }, +> }, +> }); +> +> +> +> +> ``` + +--- + +## 3. DevOps & ์ธํ”„๋ผ (6๋ฌธํ•ญ) + +### Q10. Docker Multi-stage Build + +**์งˆ๋ฌธ**: Multi-stage build์˜ ์žฅ์ ๊ณผ ๊ฐ stage ์—ญํ• ์€? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> ```dockerfile +> # Stage 1: Builder (๋นŒ๋“œ ํ™˜๊ฒฝ) +> FROM node:18-alpine AS builder +> WORKDIR /app +> COPY package.json ./ +> RUN npm install +> COPY . . +> RUN npm run build +> +> # Stage 2: Production (๋Ÿฐํƒ€์ž„) +> FROM nginx:alpine +> COPY --from=builder /app/dist /usr/share/nginx/html +> ``` +> +> **์žฅ์ **: +> - ๋นŒ๋“œ ๋„๊ตฌ ์ œ์™ธ โ†’ ์ด๋ฏธ์ง€ ํฌ๊ธฐ 90% ๊ฐ์†Œ +> - Layer caching โ†’ ๋นŒ๋“œ ์†๋„ ํ–ฅ์ƒ +> - ๋ณด์•ˆ ๊ฐ•ํ™” โ†’ ์†Œ์Šค์ฝ”๋“œ ๋ฏธํฌํ•จ + +--- + +### Q11. Kubernetes ๋ฐฐํฌ ์ „๋žต + +**์งˆ๋ฌธ**: Rolling Update, Blue/Green, Canary ์ฐจ์ด์™€ ์„ ํƒ ๊ธฐ์ค€? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> | ์ „๋žต | ํŠน์ง• | ์ ํ•ฉํ•œ ๊ฒฝ์šฐ | +> |------|------|------------| +> | **Rolling Update** | ์ ์ง„์  ๊ต์ฒด | ์ผ๋ฐ˜ ๋ฐฐํฌ, Zero-downtime | +> | **Blue/Green** | ์ „์ฒด ์ „ํ™˜ ํ›„ ์Šค์œ„์นญ | ๋น ๋ฅธ ๋กค๋ฐฑ ํ•„์š” | +> | **Canary** | ์ผ๋ถ€ ํŠธ๋ž˜ํ”ฝ ํ…Œ์ŠคํŠธ | ์œ„ํ—˜ํ•œ ๋ณ€๊ฒฝ, A/B ํ…Œ์ŠคํŠธ | +> +> **News API ๊ฐ™์€ ์ค‘์š” ์„œ๋น„์Šค**: Canary (10% โ†’ 50% โ†’ 100%) +> +> **Probe ์„ค์ •**: +> ```yaml +> livenessProbe: # ์žฌ์‹œ์ž‘ ํŒ๋‹จ +> httpGet: +> path: /health +> readinessProbe: # ํŠธ๋ž˜ํ”ฝ ์ฐจ๋‹จ ํŒ๋‹จ +> httpGet: +> path: /ready +> ``` + +--- + +### Q12. ์„œ๋น„์Šค ํ—ฌ์Šค์ฒดํฌ + +**์งˆ๋ฌธ**: Liveness Probe vs Readiness Probe ์ฐจ์ด? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> | Probe | ์‹คํŒจ ์‹œ ๋™์ž‘ | ์‹คํŒจ ์กฐ๊ฑด ์˜ˆ์‹œ | +> |-------|-------------|---------------| +> | **Liveness** | Pod ์žฌ์‹œ์ž‘ | ๋ฐ๋“œ๋ฝ, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ | +> | **Readiness** | ํŠธ๋ž˜ํ”ฝ ์ฐจ๋‹จ | DB ์—ฐ๊ฒฐ ์‹คํŒจ, ์ดˆ๊ธฐํ™” ์ค‘ | +> +> **๊ตฌํ˜„**: +> ```python +> @app.get("/health") # Liveness +> async def health(): +> return {"status": "ok"} +> +> @app.get("/ready") # Readiness +> async def ready(): +> # DB ์—ฐ๊ฒฐ ํ™•์ธ +> if not await db.ping(): +> raise HTTPException(503) +> return {"status": "ready"} +> ``` +> +> **Startup Probe**: ์ดˆ๊ธฐ ๊ตฌ๋™์ด ๋А๋ฆฐ ์•ฑ (DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋“ฑ) + +--- + +### Q13. ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ + +**์งˆ๋ฌธ**: MongoDB/Redis๋ฅผ ํด๋Ÿฌ์Šคํ„ฐ ์™ธ๋ถ€์—์„œ ์šด์˜ํ•˜๋Š” ์ด์œ ? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **ํ˜„์žฌ ์ „๋žต (์™ธ๋ถ€ ์šด์˜)**: +> - โœ… ๋ฐ์ดํ„ฐ ์˜์†์„ฑ (ํด๋Ÿฌ์Šคํ„ฐ ์žฌ์ƒ์„ฑ ์‹œ ๋ณด์กด) +> - โœ… ๊ด€๋ฆฌ ์šฉ์ด (๋‹จ์ผ ์ธ์Šคํ„ด์Šค) +> - โœ… ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ณต์œ  +> +> **StatefulSet (๋‚ด๋ถ€ ์šด์˜)**: +> - โœ… Kubernetes ํ†ตํ•ฉ ๊ด€๋ฆฌ +> - โœ… ์ž๋™ ์Šค์ผ€์ผ๋ง +> - โŒ PV ๊ด€๋ฆฌ ๋ณต์žก +> - โŒ ๋ฐฑ์—…/๋ณต๊ตฌ ๋ถ€๋‹ด +> +> **์„ ํƒ ๊ธฐ์ค€**: +> - ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง• โ†’ ์™ธ๋ถ€ (๊ฐ„ํŽธ) +> - ํ”„๋กœ๋•์…˜ โ†’ Managed Service (RDS, Atlas) ์ถ”์ฒœ + +--- + +### Q14. Docker Compose vs Kubernetes + +**์งˆ๋ฌธ**: ์–ธ์ œ Docker Compose๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•˜๊ณ  ์–ธ์ œ Kubernetes ํ•„์š”? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> | ๊ธฐ๋Šฅ | Docker Compose | Kubernetes | +> |------|---------------|-----------| +> | ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ | โœ… | โœ… | +> | Auto-scaling | โŒ | โœ… | +> | Self-healing | โŒ | โœ… | +> | Load Balancing | ๊ธฐ๋ณธ์  | ๊ณ ๊ธ‰ | +> | ๋ฐฐํฌ ์ „๋žต | ๋‹จ์ˆœ | ๋‹ค์–‘ (Rolling, Canary) | +> | ๋ฉ€ํ‹ฐ ํ˜ธ์ŠคํŠธ | โŒ | โœ… | +> +> **Docker Compose ์ถฉ๋ถ„**: +> - ๋‹จ์ผ ์„œ๋ฒ„ +> - ์†Œ๊ทœ๋ชจ ์„œ๋น„์Šค (< 10๊ฐœ) +> - ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ +> +> **Kubernetes ํ•„์š”**: +> - ๊ณ ๊ฐ€์šฉ์„ฑ (HA) +> - ์ž๋™ ํ™•์žฅ +> - ์ˆ˜์‹ญ~์ˆ˜๋ฐฑ ๊ฐœ ์„œ๋น„์Šค + +--- + +### Q15. ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… + +**์งˆ๋ฌธ**: ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ๋กœ๊ทธ ์ˆ˜์ง‘ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐฉ๋ฒ•? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **๋กœ๊น… ์Šคํƒ**: +> - **ELK**: Elasticsearch + Logstash + Kibana +> - **EFK**: Elasticsearch + Fluentd + Kibana +> - **Loki**: Grafana Loki (๊ฒฝ๋Ÿ‰) +> +> **๋ชจ๋‹ˆํ„ฐ๋ง**: +> - **Prometheus**: ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ +> - **Grafana**: ๋Œ€์‹œ๋ณด๋“œ +> - **Jaeger/Zipkin**: Distributed Tracing +> +> **Correlation ID**: +> ```python +> @app.middleware("http") +> async def add_correlation_id(request: Request, call_next): +> correlation_id = request.headers.get("X-Correlation-ID") or str(uuid.uuid4()) +> request.state.correlation_id = correlation_id +> +> # ๋ชจ๋“  ๋กœ๊ทธ์— ํฌํ•จ +> logger.info(f"Request {correlation_id}: {request.url}") +> +> response = await call_next(request) +> response.headers["X-Correlation-ID"] = correlation_id +> return response +> ``` +> +> **3๊ฐ€์ง€ ๊ด€์ฐฐ์„ฑ**: +> - Metrics (์ˆซ์ž): CPU, ๋ฉ”๋ชจ๋ฆฌ, ์š”์ฒญ ์ˆ˜ +> - Logs (ํ…์ŠคํŠธ): ์ด๋ฒคํŠธ, ์—๋Ÿฌ +> - Traces (ํ๋ฆ„): ์š”์ฒญ ๊ฒฝ๋กœ ์ถ”์  + +--- + +## 4. ๋ฐ์ดํ„ฐ ๋ฐ API ์„ค๊ณ„ (3๋ฌธํ•ญ) + +### Q16. RESTful API ์„ค๊ณ„ + +**์งˆ๋ฌธ**: News API ์—”๋“œํฌ์ธํŠธ๋ฅผ RESTfulํ•˜๊ฒŒ ์„ค๊ณ„ํ•˜๋ฉด? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> ``` +> GET /api/v1/outlets # ์–ธ๋ก ์‚ฌ ๋ชฉ๋ก +> GET /api/v1/outlets/{outlet_id} # ์–ธ๋ก ์‚ฌ ์ƒ์„ธ +> GET /api/v1/outlets/{outlet_id}/articles # ํŠน์ • ์–ธ๋ก ์‚ฌ ๊ธฐ์‚ฌ +> +> GET /api/v1/articles # ๊ธฐ์‚ฌ ๋ชฉ๋ก +> GET /api/v1/articles/{article_id} # ๊ธฐ์‚ฌ ์ƒ์„ธ +> POST /api/v1/articles # ๊ธฐ์‚ฌ ์ƒ์„ฑ +> PUT /api/v1/articles/{article_id} # ๊ธฐ์‚ฌ ์ˆ˜์ • +> DELETE /api/v1/articles/{article_id} # ๊ธฐ์‚ฌ ์‚ญ์ œ +> +> # ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ +> GET /api/v1/articles?category=tech&limit=10&offset=0 +> +> # ๋‹ค๊ตญ์–ด ์ง€์› +> GET /api/v1/ko/articles # URL prefix +> GET /api/v1/articles (Accept-Language: ko-KR) # Header +> ``` +> +> **RESTful ์›์น™**: +> 1. ๋ฆฌ์†Œ์Šค ์ค‘์‹ฌ (๋ช…์‚ฌ ์‚ฌ์šฉ) +> 2. HTTP ๋ฉ”์†Œ๋“œ ์˜๋ฏธ ์ค€์ˆ˜ +> 3. Stateless +> 4. ๊ณ„์ธต์  ๊ตฌ์กฐ +> 5. HATEOAS (์„ ํƒ) + +--- + +### Q17. MongoDB ์Šคํ‚ค๋งˆ ์„ค๊ณ„ + +**์งˆ๋ฌธ**: Outlets-Articles-Keywords ๊ด€๊ณ„๋ฅผ MongoDB์—์„œ ๋ชจ๋ธ๋ง? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **๋ฐฉ๋ฒ• 1: Embedding** (Read ์ตœ์ ํ™”) +> ```json +> { +> "_id": "article123", +> "title": "Breaking News", +> "outlet": { +> "id": "outlet456", +> "name": "TechCrunch", +> "logo": "url" +> }, +> "keywords": ["AI", "Machine Learning"] +> } +> ``` +> - โœ… 1๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ๋ชจ๋“  ๋ฐ์ดํ„ฐ +> - โŒ Outlet ์ •๋ณด ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋“  Article ์—…๋ฐ์ดํŠธ +> +> **๋ฐฉ๋ฒ• 2: Referencing** (Write ์ตœ์ ํ™”) +> ```json +> { +> "_id": "article123", +> "title": "Breaking News", +> "outlet_id": "outlet456", +> "keyword_ids": ["kw1", "kw2"] +> } +> ``` +> - โœ… ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ +> - โŒ ์กฐํšŒ ์‹œ ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ ํ•„์š” (JOIN) +> +> **ํ•˜์ด๋ธŒ๋ฆฌ๋“œ** (์ถ”์ฒœ): +> ```json +> { +> "_id": "article123", +> "title": "Breaking News", +> "outlet_id": "outlet456", +> "outlet_name": "TechCrunch", // ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ํ•„๋“œ๋งŒ ๋ณต์ œ +> "keywords": ["AI", "ML"] // ๋ฐฐ์—ด embedding +> } +> ``` +> +> **์ธ๋ฑ์‹ฑ**: +> ```python +> db.articles.create_index([("outlet_id", 1), ("published_at", -1)]) +> db.articles.create_index([("keywords", 1)]) +> ``` + +--- + +### Q18. ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ „๋žต + +**์งˆ๋ฌธ**: Offset-based vs Cursor-based Pagination ์ฐจ์ด? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **Offset-based** (์ „ํ†ต์ ): +> ```python +> # GET /api/articles?page=2&page_size=10 +> skip = (page - 1) * page_size +> articles = db.articles.find().skip(skip).limit(page_size) +> ``` +> +> - โœ… ๊ตฌํ˜„ ๊ฐ„๋‹จ, ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ํ‘œ์‹œ +> - โŒ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ ๋А๋ฆผ (SKIP ์—ฐ์‚ฐ) +> - โŒ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ค‘๋ณต/๋ˆ„๋ฝ +> +> **Cursor-based** (๋ฌดํ•œ ์Šคํฌ๋กค): +> ```python +> # GET /api/articles?cursor=article123&limit=10 +> articles = db.articles.find({ +> "_id": {"$lt": ObjectId(cursor)} +> }).sort("_id", -1).limit(10) +> +> # Response +> { +> "items": [...], +> "next_cursor": "article110" +> } +> ``` +> +> - โœ… ๋น ๋ฅธ ์„ฑ๋Šฅ (์ธ๋ฑ์Šค ํ™œ์šฉ) +> - โœ… ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ +> - โŒ ํŠน์ • ํŽ˜์ด์ง€ ์ด๋™ ๋ถˆ๊ฐ€ +> +> **์„ ํƒ ๊ธฐ์ค€**: +> - ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ํ•„์š” โ†’ Offset +> - ๋ฌดํ•œ ์Šคํฌ๋กค, ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ โ†’ Cursor + +--- + +## 5. ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ํ™•์žฅ์„ฑ (2๋ฌธํ•ญ) + +### Q19. ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ ์ฒ˜๋ฆฌ + +**์งˆ๋ฌธ**: ์ˆœ๊ฐ„ ํŠธ๋ž˜ํ”ฝ 10๋ฐฐ ์ฆ๊ฐ€ ์‹œ ๋Œ€์‘ ๋ฐฉ์•ˆ? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **1. ์บ์‹ฑ (Redis)**: +> ```python +> @app.get("/api/articles/{article_id}") +> async def get_article(article_id: str): +> # Cache-aside ํŒจํ„ด +> cached = await redis.get(f"article:{article_id}") +> if cached: +> return json.loads(cached) +> +> article = await db.articles.find_one({"_id": article_id}) +> await redis.setex(f"article:{article_id}", 3600, json.dumps(article)) +> return article +> ``` +> +> **2. Auto-scaling (HPA)**: +> ```yaml +> apiVersion: autoscaling/v2 +> kind: HorizontalPodAutoscaler +> metadata: +> name: news-api-hpa +> spec: +> scaleTargetRef: +> apiVersion: apps/v1 +> kind: Deployment +> name: news-api +> minReplicas: 2 +> maxReplicas: 10 +> metrics: +> - type: Resource +> resource: +> name: cpu +> target: +> type: Utilization +> averageUtilization: 70 +> ``` +> +> **3. Rate Limiting**: +> ```python +> from slowapi import Limiter +> +> limiter = Limiter(key_func=get_remote_address) +> +> @app.get("/api/articles") +> @limiter.limit("100/minute") +> async def list_articles(): +> ... +> ``` +> +> **4. Circuit Breaker** (์žฅ์•  ์ „ํŒŒ ๋ฐฉ์ง€): +> ```python +> from circuitbreaker import circuit +> +> @circuit(failure_threshold=5, recovery_timeout=60) +> async def call_external_service(): +> ... +> ``` +> +> **5. CDN**: ์ •์  ๋ฆฌ์†Œ์Šค (์ด๋ฏธ์ง€, CSS, JS) + +--- + +### Q20. ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค ๋Œ€์‘ + +**์งˆ๋ฌธ**: MongoDB ๋‹ค์šด/์„œ๋น„์Šค ๋ฌด์‘๋‹ต/Redis ๋ฉ”๋ชจ๋ฆฌ ๊ฐ€๋“ ์‹œ ๋Œ€์‘? + +> [!success]- ๋ชจ๋ฒ” ๋‹ต์•ˆ +> +> **1. MongoDB ๋‹ค์šด**: +> ```python +> @app.get("/api/articles") +> async def list_articles(): +> try: +> articles = await db.articles.find().to_list(10) +> return articles +> except Exception as e: +> # Graceful degradation +> logger.error(f"DB error: {e}") +> +> # Fallback: ์บ์‹œ์—์„œ ๋ฐ˜ํ™˜ +> cached = await redis.get("articles:fallback") +> if cached: +> return {"data": json.loads(cached), "source": "cache"} +> +> # ์ตœํ›„: ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ +> raise HTTPException(503, "Service temporarily unavailable") +> ``` +> +> **2. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋ฌด์‘๋‹ต**: +> ```python +> from circuitbreaker import circuit +> +> @circuit(failure_threshold=3, recovery_timeout=30) +> async def call_user_service(user_id): +> async with httpx.AsyncClient(timeout=5.0) as client: +> response = await client.get(f"http://users-service/users/{user_id}") +> return response.json() +> +> # Circuit Open ์‹œ Fallback +> try: +> user = await call_user_service(user_id) +> except CircuitBreakerError: +> # ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜ +> user = {"id": user_id, "name": "Unknown"} +> ``` +> +> **3. Redis ๋ฉ”๋ชจ๋ฆฌ ๊ฐ€๋“**: +> ```conf +> # redis.conf +> maxmemory 2gb +> maxmemory-policy allkeys-lru # LRU eviction +> ``` +> +> ```python +> # ์ค‘์š”๋„ ๊ธฐ๋ฐ˜ TTL +> await redis.setex("hot_article:123", 3600, data) # 1์‹œ๊ฐ„ +> await redis.setex("old_article:456", 300, data) # 5๋ถ„ +> ``` +> +> **Health Check ์ž๋™ ์žฌ์‹œ์ž‘**: +> ```yaml +> livenessProbe: +> httpGet: +> path: /health +> failureThreshold: 3 +> periodSeconds: 10 +> ``` + +--- + +## ํ‰๊ฐ€ ๊ธฐ์ค€ + +### ์ดˆ๊ธ‰ (Junior) - 5-8๊ฐœ ์ •๋‹ต +- ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ด +- ๊ณต์‹ ๋ฌธ์„œ ์ฐธ๊ณ ํ•˜์—ฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ +- ๊ฐ€์ด๋“œ ์žˆ์œผ๋ฉด ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ + +### ์ค‘๊ธ‰ (Mid-level) - 9-14๊ฐœ ์ •๋‹ต +- ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด ์ดํ•ด +- ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ํŒ๋‹จ ๊ฐ€๋Šฅ +- ๋…๋ฆฝ์ ์œผ๋กœ ์„œ๋น„์Šค ์„ค๊ณ„ ๋ฐ ๊ตฌํ˜„ +- ๊ธฐ๋ณธ DevOps ์ž‘์—… ๊ฐ€๋Šฅ + +### ๊ณ ๊ธ‰ (Senior) - 15-20๊ฐœ ์ •๋‹ต +- ์‹œ์Šคํ…œ ์ „์ฒด ์„ค๊ณ„ ๊ฐ€๋Šฅ +- ์„ฑ๋Šฅ/ํ™•์žฅ์„ฑ/๋ณด์•ˆ ๊ณ ๋ คํ•œ ์˜์‚ฌ๊ฒฐ์ • +- ์žฅ์•  ๋Œ€์‘ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์ „๋žต +- ํŒ€ ๋ฆฌ๋”ฉ ๋ฐ ๊ธฐ์ˆ  ๋ฉ˜ํ† ๋ง + +--- + +## ์‹ค๋ฌด ๊ณผ์ œ (์„ ํƒ) + +### ๊ณผ์ œ: Comments ์„œ๋น„์Šค ์ถ”๊ฐ€ +๊ธฐ์‚ฌ์— ๋Œ“๊ธ€ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌํ˜„ + +**์š”๊ตฌ์‚ฌํ•ญ**: +1. Backend API (FastAPI) + - CRUD ์—”๋“œํฌ์ธํŠธ + - ๋Œ€๋Œ“๊ธ€(nested comments) ์ง€์› + - ํŽ˜์ด์ง€๋„ค์ด์…˜ +2. Frontend UI (React + TypeScript) + - ๋Œ“๊ธ€ ๋ชฉ๋ก/์ž‘์„ฑ/์ˆ˜์ •/์‚ญ์ œ + - Material-UI ์‚ฌ์šฉ +3. DevOps + - Dockerfile ์ž‘์„ฑ + - Kubernetes ๋ฐฐํฌ + - Console๊ณผ ์—ฐ๋™ + +**ํ‰๊ฐ€ ์š”์†Œ**: +- ์ฝ”๋“œ ํ’ˆ์งˆ (ํƒ€์ž… ์•ˆ์ •์„ฑ, ์—๋Ÿฌ ํ•ธ๋“ค๋ง) +- API ์„ค๊ณ„ (RESTful ์›์น™) +- ์„ฑ๋Šฅ ๊ณ ๋ ค (์ธ๋ฑ์‹ฑ, ์บ์‹ฑ) +- Git ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ + +**์†Œ์š” ์‹œ๊ฐ„**: 4-6์‹œ๊ฐ„ + +--- + +## ๋ฉด์ ‘ ์ง„ํ–‰ Tips + +1. **๊นŠ์ด ์žˆ๋Š” ์งˆ๋ฌธ**: "์ด์ „ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‚˜์š”?" +2. **ํ™”์ดํŠธ๋ณด๋“œ ์„ธ์…˜**: ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ ๊ทธ๋ฆฌ๊ธฐ +3. **์ฝ”๋“œ ๋ฆฌ๋ทฐ**: ๊ธฐ์กด ์ฝ”๋“œ ๊ฐœ์„ ์  ์ฐพ๊ธฐ +4. **์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜**: "๋งŒ์•ฝ ~ํ•œ ์ƒํ™ฉ์ด๋ผ๋ฉด?" +5. **ํ›„์† ์งˆ๋ฌธ**: ๋‹ต๋ณ€์— ๋”ฐ๋ผ ์‹ฌํ™” ์งˆ๋ฌธ + +--- + +**์ž‘์„ฑ์ผ**: 2025-10-28 +**ํ”„๋กœ์ ํŠธ**: Site11 Microservices Platform +**๋Œ€์ƒ**: Full-stack Developer diff --git a/services/console/frontend/src/pages/Services.tsx b/services/console/frontend/src/pages/Services.tsx index f492f7d..ae695bd 100644 --- a/services/console/frontend/src/pages/Services.tsx +++ b/services/console/frontend/src/pages/Services.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react' import { Box, Typography, @@ -9,90 +10,391 @@ import { TableRow, Paper, Chip, + Button, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + MenuItem, + CircularProgress, + Alert, + Tooltip, } from '@mui/material' - -const servicesData = [ - { - id: 1, - name: 'Console', - type: 'API Gateway', - port: 8011, - status: 'Running', - description: 'Central orchestrator and API gateway', - }, - { - id: 2, - name: 'Users', - type: 'Microservice', - port: 8001, - status: 'Running', - description: 'User management service', - }, - { - id: 3, - name: 'MongoDB', - type: 'Database', - port: 27017, - status: 'Running', - description: 'Document database for persistence', - }, - { - id: 4, - name: 'Redis', - type: 'Cache', - port: 6379, - status: 'Running', - description: 'In-memory cache and pub/sub', - }, -] +import { + Add as AddIcon, + Edit as EditIcon, + Delete as DeleteIcon, + Refresh as RefreshIcon, + CheckCircle as HealthCheckIcon, +} from '@mui/icons-material' +import { serviceAPI } from '../api/service' +import { ServiceType, ServiceStatus } from '../types/service' +import type { Service, ServiceCreate } from '../types/service' function Services() { + const [services, setServices] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [openDialog, setOpenDialog] = useState(false) + const [editingService, setEditingService] = useState(null) + const [formData, setFormData] = useState({ + name: '', + url: '', + service_type: ServiceType.BACKEND, + description: '', + health_endpoint: '/health', + metadata: {}, + }) + + // Load services + const loadServices = async () => { + try { + setLoading(true) + setError(null) + const data = await serviceAPI.getAll() + setServices(data) + } catch (err: any) { + setError(err.response?.data?.detail || 'Failed to load services') + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadServices() + }, []) + + // Handle create/update service + const handleSave = async () => { + try { + if (editingService) { + await serviceAPI.update(editingService._id, formData) + } else { + await serviceAPI.create(formData) + } + setOpenDialog(false) + setEditingService(null) + resetForm() + loadServices() + } catch (err: any) { + setError(err.response?.data?.detail || 'Failed to save service') + } + } + + // Handle delete service + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to delete this service?')) return + + try { + await serviceAPI.delete(id) + loadServices() + } catch (err: any) { + setError(err.response?.data?.detail || 'Failed to delete service') + } + } + + // Handle health check + const handleHealthCheck = async (id: string) => { + try { + const result = await serviceAPI.checkHealth(id) + setServices(prev => prev.map(s => + s._id === id ? { ...s, status: result.status, response_time_ms: result.response_time_ms, last_health_check: result.checked_at } : s + )) + } catch (err: any) { + setError(err.response?.data?.detail || 'Failed to check health') + } + } + + // Handle health check all + const handleHealthCheckAll = async () => { + try { + await serviceAPI.checkAllHealth() + loadServices() + } catch (err: any) { + setError(err.response?.data?.detail || 'Failed to check all services') + } + } + + // Open dialog for create/edit + const openEditDialog = (service?: Service) => { + if (service) { + setEditingService(service) + setFormData({ + name: service.name, + url: service.url, + service_type: service.service_type, + description: service.description || '', + health_endpoint: service.health_endpoint || '/health', + metadata: service.metadata || {}, + }) + } else { + resetForm() + } + setOpenDialog(true) + } + + const resetForm = () => { + setFormData({ + name: '', + url: '', + service_type: ServiceType.BACKEND, + description: '', + health_endpoint: '/health', + metadata: {}, + }) + setEditingService(null) + } + + const getStatusColor = (status: ServiceStatus) => { + switch (status) { + case 'healthy': return 'success' + case 'unhealthy': return 'error' + default: return 'default' + } + } + + const getTypeColor = (type: ServiceType) => { + switch (type) { + case 'backend': return 'primary' + case 'frontend': return 'secondary' + case 'database': return 'info' + case 'cache': return 'warning' + default: return 'default' + } + } + + if (loading) { + return ( + + + + ) + } + return ( - - Services - - + + + Services + + + + + + + + + {error && ( + setError(null)} sx={{ mb: 2 }}> + {error} + + )} + Service Name Type - Port + URL Status - Description + Response Time + Last Check + Actions - {servicesData.map((service) => ( - - - {service.name} + {services.length === 0 ? ( + + + + No services found. Click "Add Service" to create one. + - - - - {service.port} - - - - {service.description} - ))} + ) : ( + services.map((service) => ( + + + {service.name} + {service.description && ( + + {service.description} + + )} + + + + + + + {service.url} + + {service.health_endpoint && ( + + Health: {service.health_endpoint} + + )} + + + + + + {service.response_time_ms ? ( + + {service.response_time_ms.toFixed(2)} ms + + ) : ( + + - + + )} + + + {service.last_health_check ? ( + + {new Date(service.last_health_check).toLocaleString()} + + ) : ( + + Never + + )} + + + + handleHealthCheck(service._id)} + color="primary" + > + + + + + openEditDialog(service)} + color="primary" + > + + + + + handleDelete(service._id)} + color="error" + > + + + + + + )) + )}
+ + {/* Add/Edit Dialog */} + setOpenDialog(false)} maxWidth="sm" fullWidth> + + {editingService ? 'Edit Service' : 'Add Service'} + + + + setFormData({ ...formData, name: e.target.value })} + required + fullWidth + /> + setFormData({ ...formData, url: e.target.value })} + required + fullWidth + placeholder="http://service-name:8000" + /> + setFormData({ ...formData, service_type: e.target.value as ServiceType })} + select + required + fullWidth + > + Backend + Frontend + Database + Cache + Message Queue + Other + + setFormData({ ...formData, health_endpoint: e.target.value })} + fullWidth + placeholder="/health" + /> + setFormData({ ...formData, description: e.target.value })} + fullWidth + multiline + rows={2} + /> + + + + + + +
) } -export default Services \ No newline at end of file +export default Services