docs: Add comprehensive technical interview guide
- 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 <noreply@anthropic.com>
This commit is contained in:
769
docs/TECHNICAL_INTERVIEW.md
Normal file
769
docs/TECHNICAL_INTERVIEW.md
Normal file
@ -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'));
|
||||||
|
>
|
||||||
|
> <Suspense fallback={<Loading />}>
|
||||||
|
> <Dashboard />
|
||||||
|
> </Suspense>
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> **테마 커스터마이징**:
|
||||||
|
> ```tsx
|
||||||
|
> import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
>
|
||||||
|
> const theme = createTheme({
|
||||||
|
> palette: {
|
||||||
|
> mode: 'dark',
|
||||||
|
> primary: { main: '#1976d2' },
|
||||||
|
> },
|
||||||
|
> });
|
||||||
|
>
|
||||||
|
> <ThemeProvider theme={theme}>
|
||||||
|
> <App />
|
||||||
|
> </ThemeProvider>
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@ -9,49 +10,206 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Paper,
|
Paper,
|
||||||
Chip,
|
Chip,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
MenuItem,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
Tooltip,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
const servicesData = [
|
Add as AddIcon,
|
||||||
{
|
Edit as EditIcon,
|
||||||
id: 1,
|
Delete as DeleteIcon,
|
||||||
name: 'Console',
|
Refresh as RefreshIcon,
|
||||||
type: 'API Gateway',
|
CheckCircle as HealthCheckIcon,
|
||||||
port: 8011,
|
} from '@mui/icons-material'
|
||||||
status: 'Running',
|
import { serviceAPI } from '../api/service'
|
||||||
description: 'Central orchestrator and API gateway',
|
import { ServiceType, ServiceStatus } from '../types/service'
|
||||||
},
|
import type { Service, ServiceCreate } from '../types/service'
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function Services() {
|
function Services() {
|
||||||
|
const [services, setServices] = useState<Service[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [openDialog, setOpenDialog] = useState(false)
|
||||||
|
const [editingService, setEditingService] = useState<Service | null>(null)
|
||||||
|
const [formData, setFormData] = useState<ServiceCreate>({
|
||||||
|
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 (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||||
Services
|
<Typography variant="h4">
|
||||||
</Typography>
|
Services
|
||||||
|
</Typography>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={loadServices}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<HealthCheckIcon />}
|
||||||
|
onClick={handleHealthCheckAll}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Check All Health
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => openEditDialog()}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Add Service
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" onClose={() => setError(null)} sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
@ -59,38 +217,182 @@ function Services() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Service Name</TableCell>
|
<TableCell>Service Name</TableCell>
|
||||||
<TableCell>Type</TableCell>
|
<TableCell>Type</TableCell>
|
||||||
<TableCell>Port</TableCell>
|
<TableCell>URL</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell>Description</TableCell>
|
<TableCell>Response Time</TableCell>
|
||||||
|
<TableCell>Last Check</TableCell>
|
||||||
|
<TableCell align="right">Actions</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{servicesData.map((service) => (
|
{services.length === 0 ? (
|
||||||
<TableRow key={service.id}>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell colSpan={7} align="center">
|
||||||
<Typography variant="subtitle2">{service.name}</Typography>
|
<Typography variant="body2" color="text.secondary" py={4}>
|
||||||
|
No services found. Click "Add Service" to create one.
|
||||||
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<Chip
|
|
||||||
label={service.type}
|
|
||||||
size="small"
|
|
||||||
color={service.type === 'API Gateway' ? 'primary' : 'default'}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{service.port}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Chip
|
|
||||||
label={service.status}
|
|
||||||
size="small"
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{service.description}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
) : (
|
||||||
|
services.map((service) => (
|
||||||
|
<TableRow key={service._id} hover>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="subtitle2">{service.name}</Typography>
|
||||||
|
{service.description && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{service.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
label={service.service_type}
|
||||||
|
size="small"
|
||||||
|
color={getTypeColor(service.service_type)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" noWrap sx={{ maxWidth: 300 }}>
|
||||||
|
{service.url}
|
||||||
|
</Typography>
|
||||||
|
{service.health_endpoint && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Health: {service.health_endpoint}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
label={service.status}
|
||||||
|
size="small"
|
||||||
|
color={getStatusColor(service.status)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{service.response_time_ms ? (
|
||||||
|
<Typography variant="body2">
|
||||||
|
{service.response_time_ms.toFixed(2)} ms
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{service.last_health_check ? (
|
||||||
|
<Typography variant="caption">
|
||||||
|
{new Date(service.last_health_check).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Never
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Tooltip title="Check Health">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleHealthCheck(service._id)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<HealthCheckIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Edit">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openEditDialog(service)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleDelete(service._id)}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
|
{/* Add/Edit Dialog */}
|
||||||
|
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingService ? 'Edit Service' : 'Add Service'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Service Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Service URL"
|
||||||
|
value={formData.url}
|
||||||
|
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
placeholder="http://service-name:8000"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Service Type"
|
||||||
|
value={formData.service_type}
|
||||||
|
onChange={(e) => setFormData({ ...formData, service_type: e.target.value as ServiceType })}
|
||||||
|
select
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<MenuItem value="backend">Backend</MenuItem>
|
||||||
|
<MenuItem value="frontend">Frontend</MenuItem>
|
||||||
|
<MenuItem value="database">Database</MenuItem>
|
||||||
|
<MenuItem value="cache">Cache</MenuItem>
|
||||||
|
<MenuItem value="message_queue">Message Queue</MenuItem>
|
||||||
|
<MenuItem value="other">Other</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
label="Health Endpoint"
|
||||||
|
value={formData.health_endpoint}
|
||||||
|
onChange={(e) => setFormData({ ...formData, health_endpoint: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
placeholder="/health"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpenDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!formData.name || !formData.url}
|
||||||
|
>
|
||||||
|
{editingService ? 'Update' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user