312 lines
6.4 KiB
Markdown
312 lines
6.4 KiB
Markdown
# 📋 부서 관리 API 문서
|
|
|
|
## 개요
|
|
|
|
회원가입 시 부서 선택을 위한 동적 부서 목록 API입니다.
|
|
|
|
---
|
|
|
|
## API 엔드포인트
|
|
|
|
### GET `/api/departments`
|
|
|
|
부서 목록을 조회합니다.
|
|
|
|
**인증**: 불필요 (공개 API)
|
|
|
|
**요청**:
|
|
```http
|
|
GET /api/departments HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**응답 (성공)**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"departments": [
|
|
{
|
|
"id": 1,
|
|
"code": "SPC",
|
|
"name": "신평차량",
|
|
"description": "신평차량사업소",
|
|
"is_active": true
|
|
},
|
|
{
|
|
"id": 2,
|
|
"code": "NPC",
|
|
"name": "노포차량",
|
|
"description": "노포차량사업소",
|
|
"is_active": true
|
|
},
|
|
{
|
|
"id": 3,
|
|
"code": "VHD",
|
|
"name": "차량처",
|
|
"description": "차량처",
|
|
"is_active": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**응답 (실패)**:
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "오류 메시지"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 프론트엔드 사용법
|
|
|
|
### Vue.js (TWA 프론트엔드)
|
|
|
|
`SignupView.vue`에서 부서 목록 로드:
|
|
|
|
```typescript
|
|
// 부서 목록 가져오기
|
|
async function fetchDepartments() {
|
|
loadingDepartments.value = true
|
|
try {
|
|
const response = await fetch('/api/departments')
|
|
|
|
if (!response.ok) {
|
|
throw new Error('부서 목록을 가져오는데 실패했습니다.')
|
|
}
|
|
|
|
const data = await response.json()
|
|
departments.value = data.departments || []
|
|
} catch (error) {
|
|
console.error('부서 목록 로딩 실패:', error)
|
|
errorMessage.value = '부서 목록을 불러오는데 실패했습니다.'
|
|
} finally {
|
|
loadingDepartments.value = false
|
|
}
|
|
}
|
|
|
|
// 컴포넌트 마운트 시 로드
|
|
onMounted(() => {
|
|
fetchDepartments()
|
|
})
|
|
```
|
|
|
|
### HTML 템플릿 사용
|
|
|
|
```html
|
|
<select v-model="formData.departmentId" required :disabled="loadingDepartments">
|
|
<option value="">
|
|
{{ loadingDepartments ? '부서 목록 로딩 중...' : '부서를 선택하세요' }}
|
|
</option>
|
|
<option v-for="dept in departments" :key="dept.id" :value="dept.id">
|
|
{{ dept.name }}
|
|
</option>
|
|
</select>
|
|
```
|
|
|
|
---
|
|
|
|
## 데이터베이스 구조
|
|
|
|
### departments 테이블
|
|
|
|
```sql
|
|
CREATE TABLE public.departments (
|
|
id SERIAL PRIMARY KEY,
|
|
code VARCHAR(20) UNIQUE NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
### 초기 데이터
|
|
|
|
```sql
|
|
INSERT INTO public.departments (code, name, description) VALUES
|
|
('SPC', '신평차량', '신평차량사업소'),
|
|
('NPC', '노포차량', '노포차량사업소'),
|
|
('VHD', '차량처', '차량처')
|
|
ON CONFLICT (code) DO NOTHING;
|
|
```
|
|
|
|
---
|
|
|
|
## 부서 추가 방법
|
|
|
|
### 1. Supabase SQL 에디터에서 직접 추가
|
|
|
|
```sql
|
|
INSERT INTO public.departments (code, name, description)
|
|
VALUES ('NEW001', '새로운부서', '부서 설명');
|
|
```
|
|
|
|
### 2. 관리자 페이지 (향후 구현)
|
|
|
|
관리자 권한이 있는 사용자가 웹 인터페이스를 통해 부서를 추가/수정/삭제할 수 있습니다.
|
|
|
|
---
|
|
|
|
## 필터링
|
|
|
|
### 활성화된 부서만 조회
|
|
|
|
현재 API는 모든 부서를 반환합니다. 비활성화된 부서를 제외하려면:
|
|
|
|
```python
|
|
@app.route("/api/departments")
|
|
def api_departments():
|
|
"""부서 목록 조회 API (활성화된 부서만)"""
|
|
try:
|
|
with build_pg_client() as c:
|
|
r = c.get(
|
|
"/departments",
|
|
params={
|
|
"select": "id,code,name,description,is_active",
|
|
"is_active": "eq.true", # 활성화된 부서만
|
|
"order": "name.asc"
|
|
}
|
|
)
|
|
r.raise_for_status()
|
|
departments = r.json() or []
|
|
|
|
return {
|
|
"success": True,
|
|
"departments": departments
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e)
|
|
}, 500
|
|
```
|
|
|
|
---
|
|
|
|
## 에러 처리
|
|
|
|
### 프론트엔드 에러 처리
|
|
|
|
```typescript
|
|
try {
|
|
const response = await fetch('/api/departments')
|
|
|
|
if (!response.ok) {
|
|
throw new Error('부서 목록을 가져오는데 실패했습니다.')
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || '알 수 없는 오류')
|
|
}
|
|
|
|
departments.value = data.departments || []
|
|
} catch (error) {
|
|
console.error('부서 목록 로딩 실패:', error)
|
|
errorMessage.value = '부서 목록을 불러오는데 실패했습니다.'
|
|
setTimeout(() => errorMessage.value = '', 3000)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 성능 최적화
|
|
|
|
### 캐싱
|
|
|
|
부서 목록은 자주 변경되지 않으므로 캐싱을 적용할 수 있습니다:
|
|
|
|
```typescript
|
|
// 간단한 메모리 캐시 (5분)
|
|
let cachedDepartments: Department[] | null = null
|
|
let cacheTime: number | null = null
|
|
const CACHE_DURATION = 5 * 60 * 1000 // 5분
|
|
|
|
async function fetchDepartments() {
|
|
const now = Date.now()
|
|
|
|
// 캐시가 유효한 경우
|
|
if (cachedDepartments && cacheTime && now - cacheTime < CACHE_DURATION) {
|
|
departments.value = cachedDepartments
|
|
return
|
|
}
|
|
|
|
// API 호출
|
|
loadingDepartments.value = true
|
|
try {
|
|
const response = await fetch('/api/departments')
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
cachedDepartments = data.departments
|
|
cacheTime = now
|
|
departments.value = data.departments
|
|
}
|
|
} finally {
|
|
loadingDepartments.value = false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트
|
|
|
|
### cURL로 테스트
|
|
|
|
```bash
|
|
curl -X GET http://localhost:5000/api/departments
|
|
```
|
|
|
|
### 브라우저 콘솔에서 테스트
|
|
|
|
```javascript
|
|
fetch('/api/departments')
|
|
.then(res => res.json())
|
|
.then(data => console.log(data))
|
|
```
|
|
|
|
---
|
|
|
|
## 문제 해결
|
|
|
|
### 부서 목록이 비어있음
|
|
|
|
**원인**: `departments` 테이블에 데이터가 없음
|
|
|
|
**해결**:
|
|
```sql
|
|
SELECT * FROM public.departments;
|
|
|
|
-- 데이터가 없으면 초기 데이터 삽입
|
|
INSERT INTO public.departments (code, name, description) VALUES
|
|
('SPC', '신평차량', '신평차량사업소'),
|
|
('NPC', '노포차량', '노포차량사업소'),
|
|
('VHD', '차량처', '차량처')
|
|
ON CONFLICT (code) DO NOTHING;
|
|
```
|
|
|
|
### API 호출 실패
|
|
|
|
**원인**: Flask 서버가 실행되지 않음 또는 CORS 오류
|
|
|
|
**해결**:
|
|
```bash
|
|
# 서버 실행 확인
|
|
python app.py
|
|
|
|
# CORS 설정 확인 (app.py)
|
|
CORS(app, origins=["*"], supports_credentials=True)
|
|
```
|
|
|
|
---
|
|
|
|
**부산교통공사 차량처**
|
|
**1호선 고장코드 시스템**
|
|
|