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