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
+
+
+ }
+ onClick={loadServices}
+ sx={{ mr: 1 }}
+ >
+ Refresh
+
+ }
+ onClick={handleHealthCheckAll}
+ variant="outlined"
+ sx={{ mr: 1 }}
+ >
+ Check All Health
+
+ }
+ onClick={() => openEditDialog()}
+ variant="contained"
+ >
+ Add Service
+
+
+
+
+ {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 */}
+
)
}
-export default Services
\ No newline at end of file
+export default Services