docs: 플로우 테스트 파일 이동 및 계획 문서 추가

- api에서 플로우 테스트 JSON 파일들 이동
- 더미 데이터 시딩 계획 추가
- 견적 자동 계산 개발 계획 추가
- 기존 계획 문서 업데이트
This commit is contained in:
2025-12-24 08:54:52 +09:00
parent 5e1e5b4fcf
commit 9b665c0d5a
26 changed files with 6764 additions and 541 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
{
"name": "계정 관리 플로우",
"description": "이용 약관 조회/동의 → 계정 일시 정지 → 회원 탈퇴 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "get_agreements",
"name": "이용 약관 목록 조회",
"method": "GET",
"endpoint": "/api/v1/account/agreements",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"agreements": "$.data"
}
},
{
"id": "update_agreements",
"name": "이용 약관 동의 상태 수정",
"method": "PUT",
"endpoint": "/api/v1/account/agreements",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"agreements": [
{
"type": "terms",
"agreed": true
},
{
"type": "privacy",
"agreed": true
},
{
"type": "marketing",
"agreed": false
}
]
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_agreements",
"name": "약관 동의 상태 확인",
"method": "GET",
"endpoint": "/api/v1/account/agreements",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "suspend_account",
"name": "계정 일시 정지",
"description": "계정을 일시 정지 상태로 변경",
"method": "POST",
"endpoint": "/api/v1/account/suspend",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"reason": "테스트용 일시 정지",
"duration_days": 30
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "withdraw_account",
"name": "회원 탈퇴 (테스트 스킵)",
"description": "회원 탈퇴 API - 실제 실행 시 계정 삭제됨 (주의)",
"method": "POST",
"endpoint": "/api/v1/account/withdraw",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"reason": "서비스 불만족",
"feedback": "테스트용 피드백입니다"
},
"expect": {
"status": [200, 400, 422],
"jsonPath": {
"$.success": "@isBoolean"
}
},
"continueOnFailure": true,
"skip": true,
"skipReason": "실제 탈퇴 실행 방지 - 수동 테스트 필요"
}
]
}

View File

@@ -0,0 +1,227 @@
{
"name": "Attendance API 근태관리 테스트",
"description": "근태 CRUD, 출퇴근 기록, 월간 통계 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_date": "{{$date}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token",
"current_user_id": "$.user.id"
}
},
{
"id": "check_in",
"name": "출근 기록",
"method": "POST",
"endpoint": "/attendances/check-in",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"check_in": "09:00:00",
"gps_data": {
"latitude": 37.5665,
"longitude": 126.978,
"accuracy": 10
}
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.status": "@isString"
}
},
"extract": {
"attendance_id": "$.data.id"
}
},
{
"id": "show_attendance",
"name": "근태 상세 조회",
"method": "GET",
"endpoint": "/attendances/{{check_in.attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{check_in.attendance_id}}",
"$.data.base_date": "@isString"
}
}
},
{
"id": "check_out",
"name": "퇴근 기록",
"method": "POST",
"endpoint": "/attendances/check-out",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"check_out": "18:00:00",
"gps_data": {
"latitude": 37.5665,
"longitude": 126.978,
"accuracy": 15
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
}
},
{
"id": "list_attendances",
"name": "근태 목록 조회",
"method": "GET",
"endpoint": "/attendances",
"query": {
"page": 1,
"per_page": 10,
"date": "{{test_date}}"
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "monthly_stats",
"name": "월간 통계 조회",
"method": "GET",
"endpoint": "/attendances/monthly-stats",
"query": {
"year": 2025,
"month": 12
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_attendance",
"name": "근태 수동 등록 (관리자)",
"method": "POST",
"endpoint": "/attendances",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"user_id": "{{login.current_user_id}}",
"base_date": "2025-12-01",
"status": "onTime",
"json_details": {
"check_in": "09:00:00",
"check_out": "18:00:00",
"work_minutes": 480
},
"remarks": "Flow Tester 테스트 데이터"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"extract": {
"manual_attendance_id": "$.data.id"
}
},
{
"id": "update_attendance",
"name": "근태 수정",
"method": "PATCH",
"endpoint": "/attendances/{{create_attendance.manual_attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"status": "late",
"remarks": "수정된 테스트 데이터"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "late"
}
}
},
{
"id": "delete_manual_attendance",
"name": "수동 등록 근태 삭제",
"method": "DELETE",
"endpoint": "/attendances/{{create_attendance.manual_attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_checkin_attendance",
"name": "출퇴근 기록 삭제 (정리)",
"method": "DELETE",
"endpoint": "/attendances/{{check_in.attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -0,0 +1,85 @@
{
"name": "인증 플로우 테스트",
"description": "로그인, 프로필 조회, 토큰 갱신, 로그아웃 플로우를 테스트합니다.",
"version": "1.0",
"config": {
"apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a",
"baseUrl": "https://api.sam.kr/api/v1",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "codebridgex",
"user_pwd": "code1234"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token",
"refreshToken": "$.refresh_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "get_profile",
"name": "프로필 조회",
"method": "GET",
"endpoint": "/users/me",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "refresh_token",
"name": "토큰 갱신",
"method": "POST",
"endpoint": "/refresh",
"body": {
"refresh_token": "{{login.refreshToken}}"
},
"dependsOn": ["get_profile"],
"extract": {
"newToken": "$.access_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "logout",
"name": "로그아웃",
"method": "POST",
"endpoint": "/logout",
"headers": {
"Authorization": "Bearer {{refresh_token.newToken}}"
},
"dependsOn": ["refresh_token"],
"expect": {
"status": [200, 204]
}
}
]
}

View File

@@ -0,0 +1,233 @@
{
"name": "부실채권 관리 플로우",
"description": "부실채권 등록 → 문서 첨부 → 메모 추가 → 상태 변경 → 삭제 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_bad_debts",
"name": "부실채권 목록 조회",
"method": "GET",
"endpoint": "/api/v1/bad-debts",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"page": 1,
"per_page": 10
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"existing_count": "$.data.total"
}
},
{
"id": "create_bad_debt",
"name": "부실채권 등록",
"method": "POST",
"endpoint": "/api/v1/bad-debts",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"client_id": 1,
"sale_id": 1,
"amount": 5000000,
"occurred_at": "2025-01-01",
"reason": "연체 90일 초과",
"status": "pending",
"description": "Flow 테스트용 부실채권"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"bad_debt_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "get_bad_debt_detail",
"name": "부실채권 상세 조회",
"method": "GET",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"continueOnFailure": true
},
{
"id": "add_document",
"name": "관련 문서 첨부",
"method": "POST",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/documents",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"file_id": 1,
"document_type": "collection_notice",
"description": "독촉장 발송 증빙"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "list_documents",
"name": "첨부 문서 목록 조회",
"method": "GET",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/documents",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "add_memo",
"name": "메모 추가",
"method": "POST",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/memos",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"content": "1차 독촉장 발송 완료",
"memo_type": "collection"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"memo_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "list_memos",
"name": "메모 목록 조회",
"method": "GET",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/memos",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "toggle_status",
"name": "상태 토글 (처리중)",
"method": "POST",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}/toggle",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"status": "in_progress"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "update_bad_debt",
"name": "부실채권 수정",
"method": "PUT",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"description": "Flow 테스트 - 수정됨",
"status": "resolved"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "delete_bad_debt",
"name": "부실채권 삭제",
"method": "DELETE",
"endpoint": "/api/v1/bad-debts/{{create_bad_debt.bad_debt_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,166 @@
{
"$schema": "flow-tester-schema.json",
"name": "분기 테스트 플로우 예시",
"description": "Flow Tester의 조건 분기 기능을 보여주는 예시 플로우입니다. 로그인 성공/실패에 따른 분기, 권한에 따른 분기, 조건부 의존성 등을 테스트합니다.",
"version": "1.0.0",
"author": "SAM Team",
"category": "examples",
"tags": ["branching", "condition", "if-else", "example"],
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": false
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "1. 로그인 시도",
"description": "사용자 인증을 시도합니다",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"expect": {
"status": [200, 401]
},
"extract": {
"token": "$.access_token",
"role": "$.user.role",
"permissions": "$.user.permissions"
},
"continueOnFailure": true
},
{
"id": "login_success_path",
"name": "2-A. 로그인 성공 처리",
"description": "로그인이 성공했을 때만 실행됩니다",
"condition": {"stepResult": "login", "is": "success"},
"dependsOn": ["login"],
"method": "GET",
"endpoint": "/api/v1/users/me",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200]
}
},
{
"id": "login_failure_path",
"name": "2-B. 로그인 실패 처리",
"description": "로그인이 실패했을 때만 실행됩니다",
"condition": {"stepResult": "login", "is": "failure"},
"dependsOn": [{"step": "login", "onlyIf": "any"}],
"method": "POST",
"endpoint": "/api/v1/auth/password-reset-request",
"body": {
"email": "{{variables.user_id}}"
},
"expect": {
"status": [200, 404]
},
"continueOnFailure": true
},
{
"id": "admin_dashboard",
"name": "3-A. 관리자 대시보드",
"description": "관리자 권한이 있을 때만 실행됩니다",
"condition": "{{login.role}} == 'admin'",
"dependsOn": ["login_success_path"],
"method": "GET",
"endpoint": "/api/v1/admin/dashboard",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200]
}
},
{
"id": "user_dashboard",
"name": "3-B. 일반 사용자 대시보드",
"description": "일반 사용자 권한일 때만 실행됩니다",
"condition": "{{login.role}} == 'user'",
"dependsOn": ["login_success_path"],
"method": "GET",
"endpoint": "/api/v1/user/dashboard",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200]
}
},
{
"id": "premium_features",
"name": "3-C. 프리미엄 기능",
"description": "프리미엄 권한이 있을 때만 실행됩니다",
"condition": {
"and": [
{"stepResult": "login", "is": "success"},
{"left": "{{login.permissions}}", "op": "contains", "right": "premium"}
]
},
"dependsOn": ["login_success_path"],
"method": "GET",
"endpoint": "/api/v1/premium/features",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200]
}
},
{
"id": "create_report",
"name": "4. 리포트 생성 (관리자만)",
"description": "관리자만 리포트를 생성할 수 있습니다",
"condition": {"stepResult": "admin_dashboard", "is": "success"},
"dependsOn": [{"step": "admin_dashboard", "onlyIf": "executed"}],
"method": "POST",
"endpoint": "/api/v1/admin/reports",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"type": "daily",
"date": "{{$faker.date}}"
},
"expect": {
"status": [201]
}
},
{
"id": "cleanup_always",
"name": "5. 정리 작업 (항상 실행)",
"description": "성공/실패와 무관하게 항상 실행되는 정리 작업입니다",
"dependsOn": [
{"step": "login", "onlyIf": "any"},
{"step": "admin_dashboard", "onlyIf": "any"},
{"step": "user_dashboard", "onlyIf": "any"}
],
"method": "POST",
"endpoint": "/api/v1/logout",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200, 401]
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,215 @@
{
"name": "Client API CRUD 테스트",
"description": "거래처(Client) API 전체 CRUD 테스트 - 생성, 조회, 수정, 토글, 삭제 포함. business_no, business_type, business_item 신규 필드 검증 포함.",
"version": "1.0",
"config": {
"baseUrl": "https://api.sam.kr/api/v1",
"apiKey": "{{$env.FLOW_TESTER_API_KEY}}",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_client_code": "TEST_CLIENT_{{$timestamp}}"
},
"steps": [
{
"id": "login",
"name": "1. 로그인 - 토큰 획득",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "create_client",
"name": "2. 거래처 생성 (신규 필드 포함)",
"method": "POST",
"endpoint": "/clients",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"client_code": "{{variables.test_client_code}}",
"name": "테스트 거래처",
"contact_person": "홍길동",
"phone": "02-1234-5678",
"email": "test@example.com",
"address": "서울시 강남구 테헤란로 123",
"business_no": "123-45-67890",
"business_type": "제조업",
"business_item": "전자부품",
"is_active": "Y"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.client_code": "{{variables.test_client_code}}",
"$.data.name": "테스트 거래처",
"$.data.business_no": "123-45-67890",
"$.data.business_type": "제조업",
"$.data.business_item": "전자부품"
}
},
"extract": {
"client_id": "$.data.id"
}
},
{
"id": "list_clients",
"name": "3. 거래처 목록 조회",
"method": "GET",
"endpoint": "/clients?page=1&size=20&q=테스트",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray",
"$.data.current_page": 1
}
}
},
{
"id": "show_client",
"name": "4. 거래처 단건 조회",
"method": "GET",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{create_client.client_id}}",
"$.data.client_code": "{{variables.test_client_code}}",
"$.data.business_no": "123-45-67890",
"$.data.business_type": "제조업",
"$.data.business_item": "전자부품"
}
}
},
{
"id": "update_client",
"name": "5. 거래처 수정 (신규 필드 변경)",
"method": "PUT",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"name": "테스트 거래처 (수정됨)",
"contact_person": "김철수",
"business_no": "987-65-43210",
"business_type": "도소매업",
"business_item": "IT솔루션"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.name": "테스트 거래처 (수정됨)",
"$.data.contact_person": "김철수",
"$.data.business_no": "987-65-43210",
"$.data.business_type": "도소매업",
"$.data.business_item": "IT솔루션"
}
}
},
{
"id": "toggle_client",
"name": "6. 거래처 활성/비활성 토글 (N으로)",
"method": "PATCH",
"endpoint": "/clients/{{create_client.client_id}}/toggle",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": false
}
}
},
{
"id": "toggle_client_back",
"name": "7. 거래처 토글 복원 (Y로)",
"method": "PATCH",
"endpoint": "/clients/{{create_client.client_id}}/toggle",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": true
}
}
},
{
"id": "list_active_only",
"name": "8. 활성 거래처만 조회",
"method": "GET",
"endpoint": "/clients?only_active=1",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "delete_client",
"name": "9. 거래처 삭제",
"method": "DELETE",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_deleted",
"name": "10. 삭제 확인 (404 예상)",
"method": "GET",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
}
]
}

View File

@@ -0,0 +1,200 @@
{
"name": "회사 가입 신청 플로우",
"description": "사업자번호 확인 → 가입 신청 → 내 신청 목록 → 승인/거절 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_business_number": "123-45-67890"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "check_business_number",
"name": "사업자번호 확인",
"description": "가입 가능한 회사인지 사업자번호로 확인",
"method": "POST",
"endpoint": "/api/v1/companies/check",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"business_number": "{{test_business_number}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"company_exists": "$.data.exists",
"company_id": "$.data.company_id"
}
},
{
"id": "submit_request",
"name": "회사 가입 신청",
"method": "POST",
"endpoint": "/api/v1/companies/request",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"company_id": "{{check_business_number.company_id}}",
"department": "개발팀",
"position": "개발자",
"message": "Flow 테스트용 가입 신청입니다."
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"request_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "get_my_requests",
"name": "내 신청 목록 조회",
"method": "GET",
"endpoint": "/api/v1/companies/my-requests",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "get_request_detail",
"name": "신청 상세 조회",
"method": "GET",
"endpoint": "/api/v1/companies/requests/{{submit_request.request_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"continueOnFailure": true
},
{
"id": "approve_request",
"name": "신청 승인 (관리자)",
"description": "관리자 권한으로 가입 신청 승인",
"method": "POST",
"endpoint": "/api/v1/companies/requests/{{submit_request.request_id}}/approve",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"role_id": 2,
"department_id": 1,
"approved_message": "승인되었습니다."
},
"expect": {
"status": [200, 403],
"jsonPath": {
"$.success": "@isBoolean"
}
},
"continueOnFailure": true
},
{
"id": "submit_another_request",
"name": "거절 테스트용 신청",
"method": "POST",
"endpoint": "/api/v1/companies/request",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"company_id": "{{check_business_number.company_id}}",
"department": "영업팀",
"position": "매니저",
"message": "거절 테스트용 신청입니다."
},
"expect": {
"status": [200, 201, 400, 422],
"jsonPath": {
"$.success": "@isBoolean"
}
},
"extract": {
"reject_request_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "reject_request",
"name": "신청 거절 (관리자)",
"description": "관리자 권한으로 가입 신청 거절",
"method": "POST",
"endpoint": "/api/v1/companies/requests/{{submit_another_request.reject_request_id}}/reject",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"reason": "테스트 거절 사유"
},
"expect": {
"status": [200, 403],
"jsonPath": {
"$.success": "@isBoolean"
}
},
"continueOnFailure": true
},
{
"id": "verify_rejection",
"name": "거절 상태 확인",
"method": "GET",
"endpoint": "/api/v1/companies/requests/{{submit_another_request.reject_request_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "rejected"
}
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,87 @@
{
"name": "Department Tree API 테스트",
"description": "부서 트리 조회 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "get_tree",
"name": "부서 트리 조회",
"method": "GET",
"endpoint": "/departments/tree",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "get_tree_with_users",
"name": "부서 트리 조회 (사용자 포함)",
"method": "GET",
"endpoint": "/departments/tree",
"query": {
"with_users": true
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "list_departments",
"name": "부서 목록 조회",
"method": "GET",
"endpoint": "/departments",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
}
]
}

View File

@@ -0,0 +1,188 @@
{
"name": "Employee API CRUD 테스트",
"description": "사원 관리 API 전체 CRUD 및 계정 생성 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_email": "test.employee.{{$timestamp}}@example.com",
"test_name": "테스트사원{{$random:4}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token",
"current_user_id": "$.user.id"
}
},
{
"id": "get_stats",
"name": "사원 통계 조회",
"method": "GET",
"endpoint": "/employees/stats",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.total": "@isNumber",
"$.data.active": "@isNumber",
"$.data.leave": "@isNumber",
"$.data.resigned": "@isNumber"
}
}
},
{
"id": "list_employees",
"name": "사원 목록 조회",
"method": "GET",
"endpoint": "/employees",
"query": {
"page": 1,
"per_page": 10
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "create_employee",
"name": "사원 등록",
"method": "POST",
"endpoint": "/employees",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"name": "{{test_name}}",
"email": "{{test_email}}",
"phone": "010-1234-5678",
"employee_number": "EMP{{$random:6}}",
"employee_status": "active",
"position": "사원",
"hire_date": "{{$date}}",
"json_extra": {
"emergency_contact": "010-9999-8888",
"address": "서울시 강남구"
}
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.user.name": "{{test_name}}"
}
},
"extract": {
"employee_id": "$.data.id",
"user_id": "$.data.user_id"
}
},
{
"id": "show_employee",
"name": "사원 상세 조회",
"method": "GET",
"endpoint": "/employees/{{create_employee.employee_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{create_employee.employee_id}}",
"$.data.employee_status": "active"
}
}
},
{
"id": "update_employee",
"name": "사원 정보 수정",
"method": "PATCH",
"endpoint": "/employees/{{create_employee.employee_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"position": "대리",
"employee_status": "active",
"json_extra": {
"emergency_contact": "010-1111-2222",
"address": "서울시 서초구",
"skills": ["Laravel", "React"]
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
}
},
{
"id": "list_filtered",
"name": "사원 필터 조회 (재직자)",
"method": "GET",
"endpoint": "/employees",
"query": {
"status": "active",
"q": "{{test_name}}"
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_employee",
"name": "사원 삭제",
"method": "DELETE",
"endpoint": "/employees/{{create_employee.employee_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -0,0 +1,287 @@
{
"name": "품목 삭제 API 테스트",
"description": "품목 삭제 시 참조 무결성 체크 및 soft delete 동작을 테스트합니다.",
"version": "1.0",
"config": {
"apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a",
"baseUrl": "https://api.sam.kr/api/v1",
"timeout": 30000,
"stopOnFailure": false
},
"variables": {
"user_id": "codebridgex",
"user_pwd": "code1234",
"testProductCode": "TEST-DEL-001",
"testProductName": "삭제 테스트용 품목",
"testBomParentCode": "TEST-BOM-PARENT-001",
"testBomParentName": "BOM 부모 품목"
},
"steps": [
{
"id": "login",
"name": "로그인",
"description": "API 테스트를 위한 로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token",
"refreshToken": "$.refresh_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "create_product_for_delete",
"name": "삭제 테스트용 품목 생성",
"description": "단순 삭제 테스트용 품목 생성 (BOM에 미사용)",
"method": "POST",
"endpoint": "/items",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"code": "{{variables.testProductCode}}",
"name": "{{variables.testProductName}}",
"unit": "EA",
"product_type": "FG",
"is_active": true
},
"dependsOn": ["login"],
"extract": {
"createdProductId": "$.data.id",
"createdProductCode": "$.data.code"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
}
},
{
"id": "get_items_list",
"name": "품목 목록 조회",
"description": "생성된 품목이 목록에 있는지 확인",
"method": "GET",
"endpoint": "/items?type=FG&search={{create_product_for_delete.createdProductCode}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["create_product_for_delete"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_product_success",
"name": "품목 삭제 (정상)",
"description": "BOM에서 사용되지 않는 품목 삭제 - 성공해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_product_for_delete.createdProductId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["get_items_list"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_deleted_not_in_list",
"name": "삭제된 품목 목록 미포함 확인",
"description": "삭제된 품목이 기본 목록에서 제외되는지 확인",
"method": "GET",
"endpoint": "/items?type=FG&search={{create_product_for_delete.createdProductCode}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_product_success"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"note": "data.total이 0이어야 함 (삭제된 품목 미포함)"
},
{
"id": "delete_already_deleted_item",
"name": "이미 삭제된 품목 재삭제 시도",
"description": "soft delete된 품목을 다시 삭제하면 404 반환해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_product_for_delete.createdProductId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["verify_deleted_not_in_list"],
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
},
{
"id": "create_bom_parent",
"name": "BOM 부모 품목 생성",
"description": "BOM 테스트를 위한 부모 품목 생성",
"method": "POST",
"endpoint": "/items",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"code": "{{variables.testBomParentCode}}",
"name": "{{variables.testBomParentName}}",
"unit": "EA",
"product_type": "FG",
"is_active": true
},
"dependsOn": ["login"],
"extract": {
"bomParentId": "$.data.id"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_bom_child",
"name": "BOM 자식 품목 생성",
"description": "BOM 구성품으로 사용될 품목 생성",
"method": "POST",
"endpoint": "/items",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"code": "TEST-BOM-CHILD-001",
"name": "BOM 자식 품목",
"unit": "EA",
"product_type": "PT",
"is_active": true
},
"dependsOn": ["create_bom_parent"],
"extract": {
"bomChildId": "$.data.id"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "add_bom_component",
"name": "BOM 구성품 추가",
"description": "부모 품목에 자식 품목을 BOM으로 등록",
"method": "POST",
"endpoint": "/items/{{create_bom_parent.bomParentId}}/bom",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"items": [
{
"ref_type": "PRODUCT",
"ref_id": "{{create_bom_child.bomChildId}}",
"quantity": 2,
"sort_order": 1
}
]
},
"dependsOn": ["create_bom_child"],
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_bom_used_item_fail",
"name": "BOM 사용 중인 품목 삭제 시도",
"description": "다른 BOM에서 구성품으로 사용 중인 품목 삭제 - 400 에러 반환해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_bom_child.bomChildId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["add_bom_component"],
"expect": {
"status": [400],
"jsonPath": {
"$.success": false,
"$.message": "@contains:BOM"
}
}
},
{
"id": "cleanup_bom",
"name": "BOM 구성품 제거",
"description": "테스트 정리 - BOM 구성품 제거",
"method": "DELETE",
"endpoint": "/items/{{create_bom_parent.bomParentId}}/bom",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_bom_used_item_fail"],
"expect": {
"status": [200, 204]
}
},
{
"id": "delete_bom_child_after_cleanup",
"name": "BOM 해제 후 자식 품목 삭제",
"description": "BOM에서 제거된 품목은 삭제 가능해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_bom_child.bomChildId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["cleanup_bom"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "cleanup_bom_parent",
"name": "BOM 부모 품목 삭제",
"description": "테스트 정리 - 부모 품목 삭제",
"method": "DELETE",
"endpoint": "/items/{{create_bom_parent.bomParentId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_bom_child_after_cleanup"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -0,0 +1,172 @@
{
"name": "ItemField is_active 컬럼 검증 테스트",
"description": "item_fields 테이블에 추가된 is_active 컬럼의 기능을 검증합니다. 필드 생성 시 기본값(true), 수정, 조회 시 is_active 필드 포함 여부를 테스트합니다.",
"version": "1.0",
"config": {
"baseUrl": "https://api.sam.kr/api/v1",
"apiKey": "{{$env.FLOW_TESTER_API_KEY}}",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "1. 로그인 - 인증 토큰 획득",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "get_fields_list",
"name": "2. 필드 목록 조회 - is_active 필드 포함 확인",
"method": "GET",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
},
"extract": {
"existingFieldId": "$.data[0].id",
"fieldCount": "$.data.length"
}
},
{
"id": "create_field",
"name": "3. 독립 필드 생성 - is_active 기본값 true 확인",
"method": "POST",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"field_name": "[테스트] is_active 검증 필드",
"field_type": "textbox",
"field_key": "test_is_active",
"is_required": false,
"placeholder": "is_active 기본값 테스트",
"description": "API Flow Tester에서 생성한 테스트 필드"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.field_name": "[테스트] is_active 검증 필드",
"$.data.is_active": true
}
},
"extract": {
"newFieldId": "$.data.id"
}
},
{
"id": "verify_field_created",
"name": "4. 생성된 필드 상세 확인 - is_active=true",
"method": "GET",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"allFields": "$.data"
}
},
{
"id": "update_field_inactive",
"name": "5. 필드 비활성화 - is_active=false로 수정",
"method": "PUT",
"endpoint": "/item-master/fields/{{create_field.newFieldId}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"is_active": false
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": false
}
}
},
{
"id": "verify_field_inactive",
"name": "6. 비활성화 상태 확인",
"method": "GET",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "update_field_active",
"name": "7. 필드 재활성화 - is_active=true로 수정",
"method": "PUT",
"endpoint": "/item-master/fields/{{create_field.newFieldId}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"is_active": true
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": true
}
}
},
{
"id": "delete_test_field",
"name": "8. 테스트 필드 삭제 (정리)",
"method": "DELETE",
"endpoint": "/item-master/fields/{{create_field.newFieldId}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -0,0 +1,157 @@
{
"name": "품목기준관리 통합 테스트",
"description": "품목기준관리 API의 전체 CRUD 플로우를 테스트합니다.",
"version": "1.0",
"config": {
"apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a",
"baseUrl": "https://api.sam.kr/api/v1",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "codebridgex",
"user_pwd": "code1234",
"testItemCode": "TEST-ITEM-{{$timestamp}}",
"testItemName": "테스트 품목",
"testItemSpec": "100x100x10",
"updatedItemName": "수정된 테스트 품목",
"updatedItemSpec": "200x200x20"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "create_item",
"name": "품목 생성",
"method": "POST",
"endpoint": "/item-master-data",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"item_code": "{{variables.testItemCode}}",
"item_name": "{{variables.testItemName}}",
"item_spec": "{{variables.testItemSpec}}",
"item_type": "PRODUCT",
"unit": "EA",
"is_active": true
},
"dependsOn": ["login"],
"extract": {
"createdItemId": "$.data.id",
"createdItemCode": "$.data.item_code"
},
"expect": {
"status": [201],
"jsonPath": {
"$.data.id": "@exists"
}
}
},
{
"id": "get_item",
"name": "품목 단건 조회",
"method": "GET",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["create_item"],
"expect": {
"status": [200],
"jsonPath": {
"$.data.id": "@exists"
}
}
},
{
"id": "list_items",
"name": "품목 목록 조회",
"method": "GET",
"endpoint": "/item-master-data",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["create_item"],
"expect": {
"status": [200],
"jsonPath": {
"$.data": "@isArray"
}
}
},
{
"id": "update_item",
"name": "품목 수정",
"method": "PUT",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"item_name": "{{variables.updatedItemName}}",
"item_spec": "{{variables.updatedItemSpec}}"
},
"dependsOn": ["get_item"],
"expect": {
"status": [200]
}
},
{
"id": "verify_update",
"name": "수정 확인",
"method": "GET",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["update_item"],
"expect": {
"status": [200]
}
},
{
"id": "delete_item",
"name": "품목 삭제",
"method": "DELETE",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["verify_update"],
"expect": {
"status": [200]
}
},
{
"id": "verify_delete",
"name": "삭제 확인",
"method": "GET",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_item"],
"expect": {
"status": [404]
}
}
]
}

View File

@@ -0,0 +1,254 @@
{
"name": "알림 설정 조회/수정 플로우",
"description": "알림 설정 조회 → 그룹별 설정 변경 → 저장 → 확인 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "get_settings",
"name": "알림 설정 조회 (그룹 기반)",
"method": "GET",
"endpoint": "/api/v1/settings/notifications",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"original_notice_enabled": "$.data.notice.enabled",
"original_approval_enabled": "$.data.approval.enabled"
}
},
{
"id": "update_notice_group",
"name": "공지사항 그룹 설정 수정",
"method": "PUT",
"endpoint": "/api/v1/settings/notifications",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"notice": {
"enabled": true,
"notice": {
"enabled": true,
"email": true
},
"event": {
"enabled": true,
"email": false
}
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_notice_update",
"name": "공지사항 설정 변경 확인",
"method": "GET",
"endpoint": "/api/v1/settings/notifications",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.notice.enabled": true
}
}
},
{
"id": "update_approval_group",
"name": "결재 그룹 설정 수정",
"method": "PUT",
"endpoint": "/api/v1/settings/notifications",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"approval": {
"enabled": true,
"approvalRequest": {
"enabled": true,
"email": true
},
"draftApproved": {
"enabled": true,
"email": false
},
"draftRejected": {
"enabled": true,
"email": true
},
"draftCompleted": {
"enabled": false,
"email": false
}
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "update_multiple_groups",
"name": "여러 그룹 동시 수정",
"method": "PUT",
"endpoint": "/api/v1/settings/notifications",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"order": {
"enabled": true,
"salesOrder": {
"enabled": true,
"email": false
},
"purchaseOrder": {
"enabled": true,
"email": false
}
},
"production": {
"enabled": false,
"safetyStock": {
"enabled": false,
"email": false
},
"productionComplete": {
"enabled": false,
"email": false
}
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "get_flat_settings",
"name": "플랫 구조 알림 설정 조회",
"method": "GET",
"endpoint": "/api/v1/users/me/notification-settings",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "update_single_type",
"name": "단일 알림 유형 수정 (플랫)",
"method": "PUT",
"endpoint": "/api/v1/users/me/notification-settings",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"notification_type": "approval",
"push_enabled": true,
"email_enabled": false,
"sms_enabled": false,
"in_app_enabled": true
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "bulk_update",
"name": "일괄 업데이트 (플랫)",
"method": "PUT",
"endpoint": "/api/v1/users/me/notification-settings/bulk",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"settings": [
{
"notification_type": "order",
"push_enabled": true,
"email_enabled": false
},
{
"notification_type": "deposit",
"push_enabled": true,
"email_enabled": true
}
]
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "final_verify",
"name": "최종 설정 확인",
"method": "GET",
"endpoint": "/api/v1/settings/notifications",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -0,0 +1,260 @@
{
"name": "결제 관리 플로우",
"description": "결제 등록 → 완료 처리 → 명세서 조회 → 취소 → 환불 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_payments",
"name": "결제 목록 조회",
"method": "GET",
"endpoint": "/api/v1/payments",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"page": 1,
"per_page": 10
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_payment",
"name": "결제 등록",
"method": "POST",
"endpoint": "/api/v1/payments",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"subscription_id": 1,
"amount": 99000,
"payment_method": "card",
"billing_name": "테스트 결제",
"billing_email": "test@example.com"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"payment_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "get_payment_detail",
"name": "결제 상세 조회",
"method": "GET",
"endpoint": "/api/v1/payments/{{create_payment.payment_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"continueOnFailure": true
},
{
"id": "complete_payment",
"name": "결제 완료 처리",
"method": "POST",
"endpoint": "/api/v1/payments/{{create_payment.payment_id}}/complete",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"transaction_id": "TXN_{{$timestamp}}",
"pg_response": {
"code": "0000",
"message": "성공"
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "verify_completed",
"name": "완료 상태 확인",
"method": "GET",
"endpoint": "/api/v1/payments/{{create_payment.payment_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "completed"
}
},
"continueOnFailure": true
},
{
"id": "get_statement",
"name": "결제 명세서 조회",
"method": "GET",
"endpoint": "/api/v1/payments/{{create_payment.payment_id}}/statement",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "cancel_payment",
"name": "결제 취소 요청",
"method": "POST",
"endpoint": "/api/v1/payments/{{create_payment.payment_id}}/cancel",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"reason": "테스트 취소",
"cancel_type": "full"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "create_payment_for_refund",
"name": "환불 테스트용 결제 등록",
"method": "POST",
"endpoint": "/api/v1/payments",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"subscription_id": 1,
"amount": 50000,
"payment_method": "card",
"billing_name": "환불 테스트",
"billing_email": "refund@example.com"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"refund_payment_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "complete_refund_payment",
"name": "환불 테스트용 결제 완료",
"method": "POST",
"endpoint": "/api/v1/payments/{{create_payment_for_refund.refund_payment_id}}/complete",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"transaction_id": "TXN_REFUND_{{$timestamp}}",
"pg_response": {
"code": "0000",
"message": "성공"
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "request_refund",
"name": "환불 요청",
"method": "POST",
"endpoint": "/api/v1/payments/{{create_payment_for_refund.refund_payment_id}}/refund",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"refund_amount": 50000,
"reason": "테스트 환불",
"refund_method": "original"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "verify_refunded",
"name": "환불 상태 확인",
"method": "GET",
"endpoint": "/api/v1/payments/{{create_payment_for_refund.refund_payment_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "refunded"
}
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,188 @@
{
"name": "팝업 관리 플로우",
"description": "팝업 등록 → 목록 조회 → 활성 팝업 조회 → 수정 → 삭제 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_popups",
"name": "팝업 목록 조회",
"method": "GET",
"endpoint": "/api/v1/popups",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"page": 1,
"per_page": 10
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_popup",
"name": "팝업 등록",
"method": "POST",
"endpoint": "/api/v1/popups",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"title": "Flow 테스트 팝업",
"content": "<p>테스트용 팝업 콘텐츠입니다.</p>",
"popup_type": "notice",
"position": "center",
"width": 500,
"height": 400,
"start_at": "2025-01-01T00:00:00Z",
"end_at": "2025-12-31T23:59:59Z",
"is_active": true,
"show_today_close": true,
"target_pages": ["dashboard", "main"]
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"popup_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "get_popup_detail",
"name": "팝업 상세 조회",
"method": "GET",
"endpoint": "/api/v1/popups/{{create_popup.popup_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"continueOnFailure": true
},
{
"id": "get_active_popups",
"name": "활성 팝업 목록 조회",
"description": "현재 표시 중인 팝업 목록",
"method": "GET",
"endpoint": "/api/v1/popups/active",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"page": "dashboard"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "update_popup",
"name": "팝업 수정",
"method": "PUT",
"endpoint": "/api/v1/popups/{{create_popup.popup_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"title": "Flow 테스트 팝업 (수정됨)",
"is_active": false
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "verify_inactive",
"name": "비활성 상태 확인",
"method": "GET",
"endpoint": "/api/v1/popups/{{create_popup.popup_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": false
}
},
"continueOnFailure": true
},
{
"id": "delete_popup",
"name": "팝업 삭제",
"method": "DELETE",
"endpoint": "/api/v1/popups/{{create_popup.popup_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "verify_deleted",
"name": "삭제 확인",
"method": "GET",
"endpoint": "/api/v1/popups/{{create_popup.popup_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [404]
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,277 @@
{
"name": "단가 관리 CRUD 테스트",
"description": "단가(Pricing) API의 생성, 조회, 수정, 확정, 삭제 전체 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_prices",
"name": "단가 목록 조회",
"method": "GET",
"endpoint": "/pricing",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"query": {
"per_page": 10,
"page": 1
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "create_price",
"name": "단가 생성 (MATERIAL)",
"method": "POST",
"endpoint": "/pricing",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"item_type_code": "MATERIAL",
"item_id": 1,
"client_group_id": null,
"purchase_price": 10000,
"processing_cost": 500,
"loss_rate": 5,
"margin_rate": 20,
"sales_price": 12600,
"rounding_rule": "round",
"rounding_unit": 100,
"supplier": "테스트 공급업체",
"effective_from": "2025-01-01",
"effective_to": "2025-12-31",
"note": "API Flow 테스트용 단가",
"status": "draft"
},
"expect": {
"status": [201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.item_type_code": "MATERIAL",
"$.data.purchase_price": 10000,
"$.data.status": "draft"
}
},
"extract": {
"price_id": "$.data.id"
}
},
{
"id": "show_price",
"name": "생성된 단가 상세 조회",
"method": "GET",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{create_price.price_id}}",
"$.data.item_type_code": "MATERIAL",
"$.data.supplier": "테스트 공급업체"
}
}
},
{
"id": "update_price",
"name": "단가 수정 (가격 변경)",
"method": "PUT",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"purchase_price": 11000,
"processing_cost": 600,
"margin_rate": 25,
"sales_price": 14500,
"note": "단가 수정 테스트",
"change_reason": "원가 인상으로 인한 가격 조정",
"status": "active"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.purchase_price": 11000,
"$.data.processing_cost": 600,
"$.data.status": "active"
}
}
},
{
"id": "get_revisions",
"name": "변경 이력 조회",
"method": "GET",
"endpoint": "/pricing/{{create_price.price_id}}/revisions",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "get_cost",
"name": "원가 조회 (receipt > standard 폴백)",
"method": "GET",
"endpoint": "/pricing/cost",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"query": {
"item_type_code": "MATERIAL",
"item_id": 1,
"date": "2025-06-15"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.item_type_code": "MATERIAL",
"$.data.item_id": 1
}
}
},
{
"id": "by_items",
"name": "다중 품목 단가 조회",
"method": "POST",
"endpoint": "/pricing/by-items",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"items": [
{
"item_type_code": "MATERIAL",
"item_id": 1
}
],
"date": "2025-06-15"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "create_price_for_finalize",
"name": "확정 테스트용 단가 생성",
"method": "POST",
"endpoint": "/pricing",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"item_type_code": "PRODUCT",
"item_id": 1,
"purchase_price": 50000,
"sales_price": 70000,
"effective_from": "2025-01-01",
"status": "active"
},
"expect": {
"status": [201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"extract": {
"finalize_price_id": "$.data.id"
}
},
{
"id": "finalize_price",
"name": "가격 확정 (불변 처리)",
"method": "POST",
"endpoint": "/pricing/{{create_price_for_finalize.finalize_price_id}}/finalize",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "finalized",
"$.data.is_final": true
}
}
},
{
"id": "delete_price",
"name": "단가 삭제 (soft delete)",
"method": "DELETE",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_deleted",
"name": "삭제된 단가 조회 시 404 확인",
"method": "GET",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
}
]
}

View File

@@ -0,0 +1,201 @@
{
"name": "매출 명세서 플로우",
"description": "매출 목록 조회 → 매출 상세 → 명세서 생성 → 확정 → 발송 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_sales",
"name": "매출 목록 조회",
"method": "GET",
"endpoint": "/api/v1/sales",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"page": 1,
"per_page": 10
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"first_sale_id": "$.data.data[0].id"
}
},
{
"id": "get_sale_detail",
"name": "매출 상세 조회",
"method": "GET",
"endpoint": "/api/v1/sales/{{list_sales.first_sale_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"extract": {
"sale_data": "$.data"
},
"continueOnFailure": true
},
{
"id": "create_sale",
"name": "매출 등록",
"method": "POST",
"endpoint": "/api/v1/sales",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"client_id": 1,
"sale_date": "2025-01-15",
"due_date": "2025-02-15",
"items": [
{
"product_id": 1,
"quantity": 10,
"unit_price": 50000,
"description": "테스트 상품"
}
],
"memo": "Flow 테스트용 매출"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"new_sale_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "get_statement",
"name": "매출 명세서 조회",
"method": "GET",
"endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}/statement",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "confirm_sale",
"name": "매출 확정",
"method": "POST",
"endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}/confirm",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"confirmed_at": "2025-01-15T10:00:00Z"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "send_statement",
"name": "명세서 발송",
"method": "POST",
"endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}/send",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"send_type": "email",
"recipient_email": "test@example.com",
"message": "매출 명세서를 발송합니다."
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "update_sale",
"name": "매출 수정",
"method": "PUT",
"endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"memo": "Flow 테스트 - 수정됨"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "delete_sale",
"name": "매출 삭제",
"method": "DELETE",
"endpoint": "/api/v1/sales/{{create_sale.new_sale_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,260 @@
{
"name": "구독 관리 플로우",
"description": "구독 등록 → 사용량 조회 → 일시 정지 → 재개 → 갱신 → 해지 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_subscriptions",
"name": "구독 목록 조회",
"method": "GET",
"endpoint": "/api/v1/subscriptions",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"page": 1,
"per_page": 10
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_subscription",
"name": "구독 등록",
"method": "POST",
"endpoint": "/api/v1/subscriptions",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"plan_id": 1,
"billing_cycle": "monthly",
"start_date": "2025-01-01",
"payment_method": "card",
"auto_renew": true
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
},
"extract": {
"subscription_id": "$.data.id"
},
"continueOnFailure": true
},
{
"id": "get_subscription_detail",
"name": "구독 상세 조회",
"method": "GET",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"continueOnFailure": true
},
{
"id": "get_usage",
"name": "사용량 조회",
"method": "GET",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/usage",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"period": "current"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "suspend_subscription",
"name": "구독 일시 정지",
"method": "POST",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/suspend",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"reason": "테스트용 일시 정지"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "verify_suspended",
"name": "정지 상태 확인",
"method": "GET",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "suspended"
}
},
"continueOnFailure": true
},
{
"id": "resume_subscription",
"name": "구독 재개",
"method": "POST",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/resume",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "renew_subscription",
"name": "구독 갱신",
"method": "POST",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/renew",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"extend_months": 1
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "update_subscription",
"name": "구독 수정",
"method": "PUT",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"auto_renew": false
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "export_usage",
"name": "사용량 내역 내보내기",
"method": "GET",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/export",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"queryParams": {
"format": "xlsx",
"period": "last_month"
},
"expect": {
"status": [200]
},
"continueOnFailure": true
},
{
"id": "cancel_subscription",
"name": "구독 해지",
"method": "POST",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}/cancel",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"reason": "테스트 완료",
"cancel_immediately": true
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"continueOnFailure": true
},
{
"id": "verify_cancelled",
"name": "해지 상태 확인",
"method": "GET",
"endpoint": "/api/v1/subscriptions/{{create_subscription.subscription_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "cancelled"
}
},
"continueOnFailure": true
}
]
}

View File

@@ -0,0 +1,125 @@
{
"name": "사용자 초대 CRUD 플로우",
"description": "사용자 초대 발송 → 목록 조회 → 재발송 → 취소 전체 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_email": "flowtest_invite_{{$timestamp}}@example.com"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "invite_user",
"name": "사용자 초대 발송 (role=user)",
"method": "POST",
"endpoint": "/api/v1/users/invite",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"email": "{{test_email}}",
"role": "user",
"message": "SAM 시스템에 합류해 주세요!"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "pending",
"$.data.email": "@isString"
}
},
"extract": {
"invitation_id": "$.data.id",
"invitation_token": "$.data.token",
"invited_email": "$.data.email"
}
},
{
"id": "list_pending",
"name": "대기 중 초대 목록 조회",
"method": "GET",
"endpoint": "/api/v1/users/invitations?status=pending&per_page=10",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "resend_invitation",
"name": "초대 재발송",
"method": "POST",
"endpoint": "/api/v1/users/invitations/{{invite_user.invitation_id}}/resend",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "pending"
}
}
},
{
"id": "cancel_invitation",
"name": "초대 취소",
"method": "DELETE",
"endpoint": "/api/v1/users/invitations/{{invite_user.invitation_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_cancelled",
"name": "취소된 초대 목록 확인",
"method": "GET",
"endpoint": "/api/v1/users/invitations?status=cancelled&per_page=10",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -1,7 +1,7 @@
# MNG 견적수식 관리 개발 계획
> **작성일**: 2025-12-22
> **상태**: 계획 수립
> **상태**: ✅ 완료
> **대상**: mng.sam.kr/quote-formulas
---
@@ -24,15 +24,9 @@
| 시뮬레이터 | ✅ 완료 | 입력값 → 계산 결과 미리보기 |
| 변수 참조 | ✅ 완료 | 사용 가능한 변수 목록 표시 |
| 수식 검증 | ✅ 완료 | 문법 검증 API |
#### 미구현/미완성 기능 (mng)
| 기능 | 상태 | 설명 |
|-----|------|-----|
| 범위(Range) 관리 UI | ❌ 미구현 | 범위별 결과 설정 화면 없음 |
| 매핑(Mapping) 관리 UI | ❌ 미구현 | 매핑 규칙 설정 화면 없음 |
| 품목(Item) 관리 UI | ❌ 미구현 | 출력 품목 설정 화면 없음 |
| 5130 데이터 연동 | ❌ 미구현 | 레거시 가격 데이터 동기화 |
| 범위(Range) 관리 UI | ✅ 완료 | 범위별 결과 설정 화면 (Phase 1) |
| 매핑(Mapping) 관리 UI | ✅ 완료 | 매핑 규칙 설정 화면 (Phase 2) |
| 품목(Item) 관리 UI | ✅ 완료 | 출력 품목 설정 화면 (Phase 3) |
### 1.2 API 프로젝트 현재 상태
@@ -52,7 +46,7 @@ QuoteFormulaCategory (카테고리)
|-----|---------|-----|
| QuoteFormulaCategorySeeder | 11개 | 카테고리 (오픈사이즈~단가수식) |
| QuoteFormulaSeeder | 30개 수식, 18개 범위 | 스크린 계산 수식 |
| QuoteFormulaItemSeeder | 25개 | 품목 마스터 (5130 가격 적용) |
| QuoteFormulaItemSeeder | 25개 | 품목 마스터 |
#### 서비스 (api)
@@ -86,41 +80,26 @@ QuoteFormulaCategory (카테고리)
|-----|-----|-----|-----|
| 수식 CRUD | ✅ | ✅ | 동일 |
| 카테고리 CRUD | ✅ | ✅ | 동일 |
| **범위 관리 UI** | | ✅ (시더) | MNG에 UI 필요 |
| **매핑 관리 UI** | | ✅ (시더) | MNG에 UI 필요 |
| **품목 관리 UI** | | ✅ (시더) | MNG에 UI 필요 |
| 범위 관리 UI | | ✅ (시더) | Phase 1 완료 |
| 매핑 관리 UI | | ✅ (시더) | Phase 2 완료 |
| 품목 관리 UI | | ✅ (시더) | Phase 3 완료 |
| 시뮬레이터 | ✅ | ✅ | 동일 |
| 자동산출 API | - | ✅ | API 전용 |
### 2.3 핵심 차이점
```
MNG (관리 UI)
├── 수식 기본 정보 관리 ✅
├── 카테고리 관리 ✅
├── 시뮬레이터 ✅
└── 범위/매핑/품목 관리 ❌ ← 개발 필요
API (자동산출 엔진)
├── 시더로 데이터 주입 ✅
├── 자동산출 서비스 ✅
└── 견적 생성 API ✅
```
---
## 3. 개발 계획
## 3. 개발 계획 (완료)
### 3.1 목표
MNG에서 **범위(Range), 매핑(Mapping), 품목(Item)** 관리 UI를 추가하여:
1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능
2. 5130 레거시 데이터를 참조하여 가격 설정 가능
2. SAM 자체 품목 마스터로 가격 설정
3. 실시간 시뮬레이션으로 설정 검증 가능
### 3.2 개발 범위
### 3.2 개발 범위 (완료)
#### Phase 1: 범위(Range) 관리 UI
#### Phase 1: 범위(Range) 관리 UI
**우선순위**: 높음
**이유**: 모터, 가이드레일, 케이스 자동 선택에 필수
@@ -160,7 +139,7 @@ DELETE /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId}
POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder
```
#### Phase 2: 매핑(Mapping) 관리 UI
#### Phase 2: 매핑(Mapping) 관리 UI
**우선순위**: 중간
**이유**: 제어기 유형 등 코드 매핑에 사용
@@ -185,7 +164,7 @@ POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder
└── [+ 매핑 추가]
```
#### Phase 3: 품목(Item) 관리 UI
#### Phase 3: 품목(Item) 관리 UI
**우선순위**: 중간
**이유**: 수식 결과로 생성되는 품목 정의
@@ -195,7 +174,7 @@ POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder
2. 품목 목록 표시
3. 품목 추가/수정/삭제
4. 수량/단가 수식 입력
5. 5130 품목 검색 및 가격 참조
5. SAM 품목 마스터에서 가격 참조
**화면 설계**:
```
@@ -207,120 +186,45 @@ POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder
│ │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1 │ 285000│
│ │ PT-GR-3000 │ 가이드레일 3000 │ 3000mm │ 2 │ 42000 │
│ └───────────────────────────────────────────────────────────┘
── [+ 품목 추가]
└── [5130에서 가져오기] ← 레거시 연동
── [+ 품목 추가]
```
#### Phase 4: 5130 데이터 연동
**우선순위**: 높음
**이유**: 실제 가격 데이터 필요
**기능 목록**:
1. 5130 DB 가격 테이블 조회 API
2. 품목 추가 시 5130 검색 모달
3. 가격 동기화 기능
4. 가격 변경 이력 관리
**5130 테이블 참조**:
```sql
-- chandj.price_motor: 모터 가격
-- chandj.price_*: 기타 품목 가격
```
### 3.3 파일 수정/추가 목록
#### Routes (mng/routes/web.php)
```php
// 기존 라우트에 추가
Route::prefix('quote-formulas')->group(function () {
// ... 기존 라우트
Route::get('/{id}/ranges', [QuoteFormulaController::class, 'ranges']);
Route::get('/{id}/mappings', [QuoteFormulaController::class, 'mappings']);
Route::get('/{id}/items', [QuoteFormulaController::class, 'items']);
});
```
#### API Routes (mng/routes/api.php)
```php
// 범위/매핑/품목 CRUD API
Route::prefix('quote-formulas/formulas/{formulaId}')->group(function () {
Route::apiResource('ranges', QuoteFormulaRangeController::class);
Route::post('ranges/reorder', [QuoteFormulaRangeController::class, 'reorder']);
Route::apiResource('mappings', QuoteFormulaMappingController::class);
Route::apiResource('items', QuoteFormulaItemController::class);
});
// 5130 연동
Route::prefix('legacy')->group(function () {
Route::get('items/search', [LegacyController::class, 'searchItems']);
Route::get('prices/{itemCode}', [LegacyController::class, 'getPrice']);
});
```
### 3.3 파일 구조 (구현 완료)
#### Controllers
```
app/Http/Controllers/
├── QuoteFormulaController.php (수정: 탭 추가)
└── Api/Admin/Quote/
├── QuoteFormulaRangeController.php (신규)
├── QuoteFormulaMappingController.php (신규)
├── QuoteFormulaItemController.php (신규)
── LegacyController.php (신규: 5130 연동)
├── QuoteFormulaController.php
├── QuoteFormulaRangeController.php
├── QuoteFormulaMappingController.php
── QuoteFormulaItemController.php
└── QuoteFormulaCategoryController.php
```
#### Services
```
app/Services/
├── Quote/
├── QuoteFormulaRangeService.php (신규)
├── QuoteFormulaMappingService.php (신규)
│ └── QuoteFormulaItemService.php (신규)
└── Legacy/
└── LegacyPriceService.php (신규: 5130 가격 조회)
app/Services/Quote/
├── QuoteFormulaService.php
├── QuoteFormulaRangeService.php
├── QuoteFormulaMappingService.php
── QuoteFormulaItemService.php
└── QuoteFormulaCategoryService.php
```
#### Views
```
resources/views/quote-formulas/
├── edit.blade.php (수정: 탭 구조로 변경)
├── partials/
│ ├── ranges-tab.blade.php (신규)
│ ├── mappings-tab.blade.php (신규)
│ └── items-tab.blade.php (신규)
└── modals/
├── range-form.blade.php (신규)
├── mapping-form.blade.php (신규)
── item-form.blade.php (신규)
└── legacy-item-search.blade.php (신규)
```
### 3.4 개발 순서
```
Phase 1: 범위 관리 (1주)
├── Day 1-2: API 엔드포인트 구현
├── Day 3-4: UI 컴포넌트 구현
└── Day 5: 테스트 및 검증
Phase 2: 매핑 관리 (0.5주)
├── Day 1: API 구현
└── Day 2-3: UI 구현
Phase 3: 품목 관리 (0.5주)
├── Day 1: API 구현
└── Day 2-3: UI 구현
Phase 4: 5130 연동 (1주)
├── Day 1-2: 레거시 DB 조회 서비스
├── Day 3-4: 검색 모달 UI
└── Day 5: 가격 동기화 기능
통합 테스트 (0.5주)
├── 시뮬레이터 연동 테스트
└── 전체 플로우 검증
├── index.blade.php
├── create.blade.php
├── edit.blade.php (수정: 탭 구조)
├── simulator.blade.php
└── partials/
├── basic-info-tab.blade.php ✅
├── ranges-tab.blade.php
├── mappings-tab.blade.php
── items-tab.blade.php
```
---
@@ -336,30 +240,17 @@ Phase 4: 5130 연동 (1주)
### 4.2 Backend (MNG)
- **Framework**: Laravel 12
- **ORM**: Eloquent
- **DB**: MySQL (samdb + chandj)
- **DB**: MySQL (samdb)
- **Auth**: Session 기반
### 4.3 API 연동
- MNG 내부 API (`/api/admin/quote-formulas/*`)
- 5130 DB 직접 조회 (chandj 데이터베이스)
---
## 5. 데이터 마이그레이션
## 5. 검증 계획
### 5.1 현재 상태
- API 시더로 30개 수식, 18개 범위, 25개 품목 등록됨
- 5130 실제 가격 데이터 반영 완료
### 5.2 마이그레이션 계획
1. **Phase 1 완료 후**: 시더 데이터를 MNG UI로 확인 가능
2. **Phase 4 완료 후**: 5130 데이터 자동 동기화
---
## 6. 검증 계획
### 6.1 시뮬레이터 테스트
### 5.1 시뮬레이터 테스트
```
입력: W0=3000, H0=2500
예상 결과:
@@ -368,22 +259,31 @@ Phase 4: 5130 연동 (1주)
- MOTOR: PT-MOTOR-150 (K=41.21kg)
```
### 6.2 CRUD 테스트
### 5.2 CRUD 테스트
- 범위 추가/수정/삭제 후 시뮬레이터 결과 확인
- 품목 가격 변경 후 합계 확인
---
## 7. 참고 자료
## 6. 참고 자료
### 7.1 기존 파일 위치 (MNG)
### 6.1 파일 위치 (MNG)
```
mng/
├── app/Http/Controllers/
│ ├── QuoteFormulaController.php
│ └── Api/Admin/Quote/QuoteFormulaController.php
│ └── Api/Admin/Quote/
│ ├── QuoteFormulaController.php
│ ├── QuoteFormulaRangeController.php
│ ├── QuoteFormulaMappingController.php
│ ├── QuoteFormulaItemController.php
│ └── QuoteFormulaCategoryController.php
├── app/Services/Quote/
── QuoteFormulaService.php
── QuoteFormulaService.php
│ ├── QuoteFormulaRangeService.php
│ ├── QuoteFormulaMappingService.php
│ ├── QuoteFormulaItemService.php
│ └── QuoteFormulaCategoryService.php
├── app/Models/Quote/
│ ├── QuoteFormula.php
│ ├── QuoteFormulaCategory.php
@@ -394,10 +294,15 @@ mng/
├── index.blade.php
├── create.blade.php
├── edit.blade.php
── simulator.blade.php
── simulator.blade.php
└── partials/
├── basic-info-tab.blade.php
├── ranges-tab.blade.php
├── mappings-tab.blade.php
└── items-tab.blade.php
```
### 7.2 API 시더 위치
### 6.2 API 시더 위치
```
api/database/seeders/
├── QuoteFormulaCategorySeeder.php
@@ -405,35 +310,11 @@ api/database/seeders/
└── QuoteFormulaItemSeeder.php
```
### 7.3 5130 가격 테이블
```sql
chandj.price_motor -- 모터 가격
chandj.price_* -- 기타 품목 가격 (확인 필요)
```
---
## 8. 리스크 및 대응
## 7. 코딩 컨벤션 및 예시 코드
| 리스크 | 영향 | 대응 |
|-------|-----|-----|
| 5130 DB 스키마 변경 | 중 | JSON 파싱 로직 유연하게 구현 |
| MNG-API 데이터 불일치 | 높 | 동일 DB 사용으로 해결됨 |
| 시뮬레이터 성능 저하 | 낮 | 수식 캐싱 적용 |
---
## 9. 다음 단계
1. **승인 요청**: 이 계획에 대한 검토 및 승인
2. **Phase 1 착수**: 범위 관리 UI 개발 시작
3. **주간 리뷰**: 진행 상황 점검
---
## 10. 코딩 컨벤션 및 예시 코드
### 10.1 API Controller 패턴 (MNG)
### 7.1 API Controller 패턴 (MNG)
```php
<?php
@@ -541,7 +422,7 @@ class QuoteFormulaRangeController extends Controller
}
```
### 10.2 Service 패턴 (MNG)
### 7.2 Service 패턴 (MNG)
```php
<?php
@@ -611,284 +492,7 @@ class QuoteFormulaRangeService
}
```
### 10.3 Model 구조 (참조용)
```php
<?php
// 파일: app/Models/Quote/QuoteFormulaRange.php (기존)
class QuoteFormulaRange extends Model
{
protected $table = 'quote_formula_ranges';
protected $fillable = [
'formula_id',
'min_value',
'max_value',
'condition_variable',
'result_value', // JSON 저장 가능: {"value": "150K", "item_code": "PT-MOTOR-150"}
'result_type', // 'fixed' | 'formula'
'sort_order',
];
protected $casts = [
'min_value' => 'decimal:4',
'max_value' => 'decimal:4',
'sort_order' => 'integer',
];
public function formula(): BelongsTo
{
return $this->belongsTo(QuoteFormula::class, 'formula_id');
}
public function isInRange($value): bool
{
// min < value <= max 체크
}
}
```
### 10.4 Blade View 패턴 (HTMX + Alpine.js)
```blade
{{-- 파일: resources/views/quote-formulas/partials/ranges-tab.blade.php --}}
<div x-data="rangesManager()" x-init="loadRanges()">
{{-- 헤더 --}}
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">범위 설정</h3>
<button @click="openAddModal()" class="btn btn-primary btn-sm">
+ 범위 추가
</button>
</div>
{{-- 조건 변수 표시 --}}
<div class="mb-4">
<span class="text-sm text-gray-600">조건 변수:</span>
<span class="font-mono bg-gray-100 px-2 py-1 rounded" x-text="conditionVariable || '-'"></span>
</div>
{{-- 범위 목록 테이블 --}}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="w-12">#</th>
<th>최소값</th>
<th>최대값</th>
<th>결과값</th>
<th>품목코드</th>
<th class="w-24">액션</th>
</tr>
</thead>
<tbody>
<template x-for="(range, index) in ranges" :key="range.id">
<tr>
<td x-text="index + 1"></td>
<td x-text="range.min_value ?? '-'"></td>
<td x-text="range.max_value ?? '-'"></td>
<td>
<span class="font-mono" x-text="getResultDisplay(range)"></span>
</td>
<td>
<span class="badge badge-ghost" x-text="getItemCode(range) || '-'"></span>
</td>
<td>
<button @click="editRange(range)" class="btn btn-ghost btn-xs">수정</button>
<button @click="deleteRange(range.id)" class="btn btn-ghost btn-xs text-red-500">삭제</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
{{-- 빈 상태 --}}
<div x-show="ranges.length === 0" class="text-center py-8 text-gray-500">
설정된 범위가 없습니다.
</div>
{{-- 범위 추가/수정 모달 --}}
<dialog id="rangeModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg" x-text="editingRange ? '범위 수정' : '범위 추가'"></h3>
<form @submit.prevent="saveRange()">
<div class="grid grid-cols-2 gap-4 mt-4">
<div>
<label class="label">최소값</label>
<input type="number" step="0.0001" x-model="form.min_value"
class="input input-bordered w-full">
</div>
<div>
<label class="label">최대값</label>
<input type="number" step="0.0001" x-model="form.max_value"
class="input input-bordered w-full">
</div>
</div>
<div class="mt-4">
<label class="label">결과값 (JSON)</label>
<textarea x-model="form.result_value" rows="3"
class="textarea textarea-bordered w-full font-mono"
placeholder='{"value": "150K", "item_code": "PT-MOTOR-150"}'></textarea>
</div>
<div class="modal-action">
<button type="button" @click="closeModal()" class="btn">취소</button>
<button type="submit" class="btn btn-primary">저장</button>
</div>
</form>
</div>
</dialog>
</div>
<script>
function rangesManager() {
return {
ranges: [],
conditionVariable: '',
editingRange: null,
form: {
min_value: null,
max_value: null,
result_value: '',
result_type: 'fixed',
},
async loadRanges() {
const formulaId = {{ $formula->id }};
const res = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}/ranges`);
const data = await res.json();
if (data.success) {
this.ranges = data.data;
if (this.ranges.length > 0) {
this.conditionVariable = this.ranges[0].condition_variable;
}
}
},
getResultDisplay(range) {
try {
const parsed = JSON.parse(range.result_value);
return parsed.value || range.result_value;
} catch {
return range.result_value;
}
},
getItemCode(range) {
try {
const parsed = JSON.parse(range.result_value);
return parsed.item_code;
} catch {
return null;
}
},
openAddModal() {
this.editingRange = null;
this.form = { min_value: null, max_value: null, result_value: '', result_type: 'fixed' };
document.getElementById('rangeModal').showModal();
},
editRange(range) {
this.editingRange = range;
this.form = { ...range };
document.getElementById('rangeModal').showModal();
},
closeModal() {
document.getElementById('rangeModal').close();
},
async saveRange() {
const formulaId = {{ $formula->id }};
const url = this.editingRange
? `/api/admin/quote-formulas/formulas/${formulaId}/ranges/${this.editingRange.id}`
: `/api/admin/quote-formulas/formulas/${formulaId}/ranges`;
const method = this.editingRange ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
body: JSON.stringify(this.form),
});
const data = await res.json();
if (data.success) {
this.closeModal();
await this.loadRanges();
showToast(data.message, 'success');
} else {
showToast(data.message, 'error');
}
},
async deleteRange(rangeId) {
if (!confirm('이 범위를 삭제하시겠습니까?')) return;
const formulaId = {{ $formula->id }};
const res = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}/ranges/${rangeId}`, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
});
const data = await res.json();
if (data.success) {
await this.loadRanges();
showToast(data.message, 'success');
}
},
};
}
</script>
```
### 10.5 DB 스키마 (참조용)
```sql
-- quote_formula_ranges 테이블 (기존)
CREATE TABLE `quote_formula_ranges` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`formula_id` BIGINT UNSIGNED NOT NULL,
`min_value` DECIMAL(15,4) NULL COMMENT '최소값 (NULL=제한없음)',
`max_value` DECIMAL(15,4) NULL COMMENT '최대값 (NULL=제한없음)',
`condition_variable` VARCHAR(50) NOT NULL COMMENT '조건 변수 (K, H1, S 등)',
`result_value` TEXT NOT NULL COMMENT '결과값 (JSON 또는 문자열)',
`result_type` ENUM('fixed','formula') DEFAULT 'fixed' COMMENT '결과 유형',
`sort_order` INT DEFAULT 0,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `idx_formula_id` (`formula_id`),
CONSTRAINT `fk_ranges_formula` FOREIGN KEY (`formula_id`)
REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE
);
-- quote_formula_items 테이블 (기존)
CREATE TABLE `quote_formula_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`formula_id` BIGINT UNSIGNED NOT NULL,
`item_code` VARCHAR(50) NOT NULL COMMENT '품목 코드',
`item_name` VARCHAR(200) NOT NULL COMMENT '품목명',
`specification` VARCHAR(200) NULL COMMENT '규격',
`unit` VARCHAR(20) NOT NULL COMMENT '단위',
`quantity_formula` VARCHAR(500) NOT NULL COMMENT '수량 계산식',
`unit_price_formula` VARCHAR(500) NULL COMMENT '단가 계산식 (NULL=마스터 참조)',
`sort_order` INT DEFAULT 0,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
INDEX `idx_formula_id` (`formula_id`),
INDEX `idx_item_code` (`item_code`),
CONSTRAINT `fk_items_formula` FOREIGN KEY (`formula_id`)
REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE
);
```
### 10.6 API 응답 형식
### 7.3 API 응답 형식
```json
// 성공 응답
@@ -924,29 +528,26 @@ CREATE TABLE `quote_formula_items` (
---
## 11. 체크리스트 (새 세션용)
## 8. 체크리스트 (완료)
### 개발 시작 전 확인
### 개발 완료 확인
- [ ] mng 프로젝트 디렉토리 확인: `/Users/hskwon/Works/@KD_SAM/SAM/mng`
- [ ] 기존 Controller 패턴 확인: `app/Http/Controllers/Api/Admin/Quote/QuoteFormulaController.php`
- [ ] 기존 Model 확인: `app/Models/Quote/QuoteFormulaRange.php`
- [ ] 기존 View 확인: `resources/views/quote-formulas/edit.blade.php`
- [ ] DB 테이블 확인: `quote_formula_ranges`, `quote_formula_items`
### Phase 1 (범위 관리) 작업 순서
1. [ ] `QuoteFormulaRangeController.php` 생성
2. [ ] `QuoteFormulaRangeService.php` 생성
3. [ ] `routes/api.php`에 라우트 추가
4. [ ] `edit.blade.php` 탭 구조로 수정
5. [ ] `partials/ranges-tab.blade.php` 생성
6. [ ] HTMX/Alpine.js 연동 테스트
7. [ ] 시뮬레이터와 연동 확인
- [x] mng 프로젝트 디렉토리: `/Users/hskwon/Works/@KD_SAM/SAM/mng`
- [x] `QuoteFormulaRangeController.php` 생성
- [x] `QuoteFormulaRangeService.php` 생성
- [x] `QuoteFormulaMappingController.php` 생성
- [x] `QuoteFormulaMappingService.php` 생성
- [x] `QuoteFormulaItemController.php` 생성
- [x] `QuoteFormulaItemService.php` 생성
- [x] `routes/api.php`에 라우트 추가
- [x] `edit.blade.php` 탭 구조로 수정
- [x] `partials/ranges-tab.blade.php` 생성
- [x] `partials/mappings-tab.blade.php` 생성
- [x] `partials/items-tab.blade.php` 생성
---
*문서 버전*: 1.1
*문서 버전*: 2.0
*작성자*: Claude Code
*검토자*: -
*업데이트*: 코딩 컨벤션 및 예시 코드 추가 (2025-12-22)
*최종 업데이트*: 2025-12-22 (Phase 1-3 완료, 5130 연동 제거)

View File

@@ -0,0 +1,743 @@
# 견적 자동산출 개발 계획
> **작성일**: 2025-12-22
> **상태**: ✅ 구현 완료
> **목표**: MNG 견적수식 데이터 셋팅 + React 견적관리 자동산출 기능 구현
> **완료일**: 2025-12-22
> **실제 소요 시간**: 약 2시간
---
## 0. 빠른 시작 가이드
### 폴더 구조 이해 (중요!)
| 폴더 | 포트 | 역할 | 비고 |
|------|------|------|------|
| `design/` | localhost:3002 | 디자인 프로토타입 | UI 참고용 |
| `react/` | localhost:3000 | **실제 프론트엔드** | 구현 대상 ✅ |
| `mng/` | mng.sam.kr | 관리자 패널 | 수식 데이터 관리 |
| `api/` | api.sam.kr | REST API | 견적 산출 엔진 |
### 이 문서만으로 작업을 시작하려면:
```bash
# 1. Docker 서비스 시작
cd /Users/hskwon/Works/@KD_SAM/SAM
docker-compose up -d
# 2. MNG 시더 실행 (Phase 1 완료 후)
cd mng
php artisan quote:seed-formulas --tenant=1
# 3. React 개발 서버 (실제 구현 대상)
cd react
npm run dev
# http://localhost:3000 접속
```
### 핵심 파일 위치
| 구분 | 파일 경로 | 역할 |
|------|----------|------|
| **MNG 시더** | `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | 🆕 생성 필요 |
| **React 자동산출** | `react/src/components/quotes/QuoteRegistration.tsx` | ⚡ 수정 필요 (line 332) |
| **API 클라이언트** | `react/src/lib/api/client.ts` | 참조 |
| **API 엔드포인트** | `api/app/Http/Controllers/Api/V1/QuoteController.php` | ✅ 구현됨 |
| **수식 엔진** | `api/app/Services/Quote/QuoteCalculationService.php` | ✅ 구현됨 |
---
## 1. 현황 분석
### 1.1 시스템 구조
```
┌───────────────────────────────────────────────────────────────────────────────┐
│ SAM 시스템 │
├───────────────────────────────────────────────────────────────────────────────┤
│ MNG (mng.sam.kr) │ React (react/ 폴더) │ Design │
│ ├── 기준정보관리 │ ├── 판매관리 │ (참고용) │
│ │ └── 견적수식관리 ✅ │ │ └── 견적관리 │ │
│ │ - 카테고리 CRUD │ │ └── 자동견적산출 │ design/ │
│ │ - 수식 CRUD │ │ UI 있음 ✅ │ :3002 │
│ │ - 범위/매핑/품목 탭 │ │ API 연동 ❌ │ │
│ │ │ │ │ │
│ └── DB: quote_formulas 테이블 │ └── API 호출: │ │
│ (데이터 없음! ❌) │ POST /v1/quotes/calculate │ │
└───────────────────────────────────────────────────────────────────────────────┘
※ design/ 폴더 (localhost:3002)는 UI 프로토타입용이며, 실제 구현은 react/ 폴더에서 진행
```
### 1.2 React 견적등록 컴포넌트 현황
**파일**: `react/src/components/quotes/QuoteRegistration.tsx`
```typescript
// 현재 상태 (line 332-335)
const handleAutoCalculate = () => {
toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`);
};
// 입력 필드 (이미 구현됨):
interface QuoteItem {
openWidth: string; // W0 (오픈사이즈 가로)
openHeight: string; // H0 (오픈사이즈 세로)
productCategory: string; // screen | steel
quantity: number;
// ... 기타 필드
}
```
### 1.3 API 엔드포인트 현황
**파일**: `api/app/Http/Controllers/Api/V1/QuoteController.php`
```php
// 이미 구현됨 (line 135-145)
public function calculate(QuoteCalculateRequest $request)
{
return ApiResponse::handle(function () use ($request) {
$validated = $request->validated();
return $this->calculationService->calculate(
$validated['inputs'] ?? $validated,
$validated['product_category'] ?? null
);
}, __('message.quote.calculated'));
}
```
### 1.4 수식 시더 데이터 (API)
**파일**: `api/database/seeders/QuoteFormulaSeeder.php`
| 카테고리 | 수식 수 | 설명 |
|---------|--------|------|
| OPEN_SIZE | 2 | W0, H0 입력값 |
| MAKE_SIZE | 4 | 제작사이즈 계산 |
| AREA | 1 | 면적 = W1 * H1 / 1000000 |
| WEIGHT | 2 | 중량 계산 (스크린/철재) |
| GUIDE_RAIL | 5 | 가이드레일 자동 선택 |
| CASE | 3 | 케이스 자동 선택 |
| MOTOR | 1 | 모터 자동 선택 (범위 9개) |
| CONTROLLER | 2 | 제어기 매핑 |
| EDGE_WING | 1 | 마구리 수량 |
| INSPECTION | 1 | 검사비 |
| PRICE_FORMULA | 8 | 단가 수식 |
| **합계** | **30개** | + 범위 18개 |
---
## 2. 개발 상세 계획
### Phase 1: MNG 시더 데이터 생성 (1일)
#### 2.1 Artisan 명령어 생성
**생성할 파일**: `mng/app/Console/Commands/SeedQuoteFormulasCommand.php`
```php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SeedQuoteFormulasCommand extends Command
{
protected $signature = 'quote:seed-formulas
{--tenant=1 : 테넌트 ID}
{--only= : categories|formulas|ranges|mappings|items}
{--fresh : 기존 데이터 삭제 후 재생성}';
protected $description = '견적수식 시드 데이터를 생성합니다';
public function handle(): int
{
$tenantId = (int) $this->option('tenant');
$only = $this->option('only');
$fresh = $this->option('fresh');
if ($fresh) {
$this->warn('기존 데이터를 삭제합니다...');
$this->truncateTables($tenantId);
}
if (!$only || $only === 'categories') {
$this->seedCategories($tenantId);
}
if (!$only || $only === 'formulas') {
$this->seedFormulas($tenantId);
}
if (!$only || $only === 'ranges') {
$this->seedRanges($tenantId);
}
$this->info('✅ 견적수식 시드 완료!');
return Command::SUCCESS;
}
private function seedCategories(int $tenantId): void
{
$categories = [
['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'sort_order' => 1],
['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'sort_order' => 2],
['code' => 'AREA', 'name' => '면적', 'sort_order' => 3],
['code' => 'WEIGHT', 'name' => '중량', 'sort_order' => 4],
['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'sort_order' => 5],
['code' => 'CASE', 'name' => '케이스', 'sort_order' => 6],
['code' => 'MOTOR', 'name' => '모터', 'sort_order' => 7],
['code' => 'CONTROLLER', 'name' => '제어기', 'sort_order' => 8],
['code' => 'EDGE_WING', 'name' => '마구리', 'sort_order' => 9],
['code' => 'INSPECTION', 'name' => '검사', 'sort_order' => 10],
['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'sort_order' => 11],
];
foreach ($categories as $cat) {
DB::table('quote_formula_categories')->updateOrInsert(
['tenant_id' => $tenantId, 'code' => $cat['code']],
array_merge($cat, [
'tenant_id' => $tenantId,
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
])
);
}
$this->info("카테고리 " . count($categories) . "개 생성됨");
}
private function seedFormulas(int $tenantId): void
{
// API 시더와 동일한 데이터 (api/database/seeders/QuoteFormulaSeeder.php 참조)
$formulas = $this->getFormulaData();
$categoryMap = DB::table('quote_formula_categories')
->where('tenant_id', $tenantId)
->pluck('id', 'code')
->toArray();
$count = 0;
foreach ($formulas as $formula) {
$categoryId = $categoryMap[$formula['category_code']] ?? null;
if (!$categoryId) continue;
DB::table('quote_formulas')->updateOrInsert(
['tenant_id' => $tenantId, 'variable' => $formula['variable']],
[
'tenant_id' => $tenantId,
'category_id' => $categoryId,
'variable' => $formula['variable'],
'name' => $formula['name'],
'type' => $formula['type'],
'formula' => $formula['formula'] ?? null,
'output_type' => 'variable',
'description' => $formula['description'] ?? null,
'sort_order' => $formula['sort_order'] ?? 0,
'is_active' => $formula['is_active'] ?? true,
'created_at' => now(),
'updated_at' => now(),
]
);
$count++;
}
$this->info("수식 {$count}개 생성됨");
}
private function getFormulaData(): array
{
return [
// 오픈사이즈
['category_code' => 'OPEN_SIZE', 'variable' => 'W0', 'name' => '오픈사이즈 W0 (가로)', 'type' => 'input', 'formula' => null, 'sort_order' => 1],
['category_code' => 'OPEN_SIZE', 'variable' => 'H0', 'name' => '오픈사이즈 H0 (세로)', 'type' => 'input', 'formula' => null, 'sort_order' => 2],
// 제작사이즈
['category_code' => 'MAKE_SIZE', 'variable' => 'W1_SCREEN', 'name' => '제작사이즈 W1 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 140', 'sort_order' => 1],
['category_code' => 'MAKE_SIZE', 'variable' => 'H1_SCREEN', 'name' => '제작사이즈 H1 (스크린)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 2],
['category_code' => 'MAKE_SIZE', 'variable' => 'W1_STEEL', 'name' => '제작사이즈 W1 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 110', 'sort_order' => 3],
['category_code' => 'MAKE_SIZE', 'variable' => 'H1_STEEL', 'name' => '제작사이즈 H1 (철재)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 4],
// 면적
['category_code' => 'AREA', 'variable' => 'M', 'name' => '면적 계산', 'type' => 'calculation', 'formula' => 'W1 * H1 / 1000000', 'sort_order' => 1],
// 중량
['category_code' => 'WEIGHT', 'variable' => 'K_SCREEN', 'name' => '중량 계산 (스크린)', 'type' => 'calculation', 'formula' => 'M * 2 + W0 / 1000 * 14.17', 'sort_order' => 1],
['category_code' => 'WEIGHT', 'variable' => 'K_STEEL', 'name' => '중량 계산 (철재)', 'type' => 'calculation', 'formula' => 'M * 25', 'sort_order' => 2],
// 가이드레일
['category_code' => 'GUIDE_RAIL', 'variable' => 'G', 'name' => '가이드레일 제작길이', 'type' => 'calculation', 'formula' => 'H0 + 250', 'sort_order' => 1],
['category_code' => 'GUIDE_RAIL', 'variable' => 'GR_AUTO_SELECT', 'name' => '가이드레일 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 2],
// 케이스
['category_code' => 'CASE', 'variable' => 'S_SCREEN', 'name' => '케이스 사이즈 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 220', 'sort_order' => 1],
['category_code' => 'CASE', 'variable' => 'S_STEEL', 'name' => '케이스 사이즈 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 240', 'sort_order' => 2],
['category_code' => 'CASE', 'variable' => 'CASE_AUTO_SELECT', 'name' => '케이스 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 3],
// 모터
['category_code' => 'MOTOR', 'variable' => 'MOTOR_AUTO_SELECT', 'name' => '모터 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 1],
// 제어기
['category_code' => 'CONTROLLER', 'variable' => 'CONTROLLER_TYPE', 'name' => '제어기 유형', 'type' => 'input', 'formula' => null, 'sort_order' => 0],
['category_code' => 'CONTROLLER', 'variable' => 'CTRL_AUTO_SELECT', 'name' => '제어기 자동 선택', 'type' => 'mapping', 'formula' => null, 'sort_order' => 1],
// 검사
['category_code' => 'INSPECTION', 'variable' => 'INSP_FEE', 'name' => '검사비', 'type' => 'calculation', 'formula' => '1', 'sort_order' => 1],
];
}
// ... 나머지 메서드 (seedRanges, truncateTables 등)
}
```
#### 2.2 작업 순서
```bash
# 1. 명령어 파일 생성
# mng/app/Console/Commands/SeedQuoteFormulasCommand.php
# 2. 실행
cd mng
php artisan quote:seed-formulas --tenant=1
# 3. 확인
php artisan tinker
>>> \App\Models\Quote\QuoteFormula::count()
# 예상: 30
# 4. 시뮬레이터 테스트
# mng.sam.kr/quote-formulas/simulator
# 입력: W0=3000, H0=2500
```
---
### Phase 2: React 자동산출 기능 구현 (2-3일)
#### 2.1 API 클라이언트 추가
**수정할 파일**: `react/src/lib/api/quote.ts` (신규)
```typescript
// react/src/lib/api/quote.ts
import { ApiClient } from './client';
import { AUTH_CONFIG } from './auth/auth-config';
// API 응답 타입
interface CalculationResult {
inputs: Record<string, number | string>;
outputs: Record<string, {
name: string;
value: number;
category: string;
type: string;
}>;
items: Array<{
item_code: string;
item_name: string;
specification?: string;
unit?: string;
quantity: number;
unit_price: number;
total_price: number;
formula_variable: string;
}>;
costs: {
material_cost: number;
labor_cost: number;
install_cost: number;
subtotal: number;
};
errors: string[];
}
interface CalculateRequest {
inputs: {
W0: number;
H0: number;
QTY?: number;
INSTALL_TYPE?: string;
CONTROL_TYPE?: string;
};
product_category: 'screen' | 'steel';
}
// Quote API 클라이언트
class QuoteApiClient extends ApiClient {
constructor() {
super({
mode: 'bearer',
apiKey: AUTH_CONFIG.apiKey,
getToken: () => {
if (typeof window !== 'undefined') {
return localStorage.getItem('auth_token');
}
return null;
},
});
}
/**
* 자동 견적 산출
*/
async calculate(request: CalculateRequest): Promise<{ success: boolean; data: CalculationResult; message: string }> {
return this.post('/api/v1/quotes/calculate', request);
}
/**
* 입력 스키마 조회
*/
async getCalculationSchema(productCategory?: string): Promise<{ success: boolean; data: Record<string, unknown> }> {
const query = productCategory ? `?product_category=${productCategory}` : '';
return this.get(`/api/v1/quotes/calculation-schema${query}`);
}
}
export const quoteApi = new QuoteApiClient();
```
#### 2.2 QuoteRegistration.tsx 수정
**수정할 파일**: `react/src/components/quotes/QuoteRegistration.tsx`
```typescript
// 추가할 import
import { quoteApi } from '@/lib/api/quote';
import { useState } from 'react';
// 상태 추가 (컴포넌트 내부)
const [calculationResult, setCalculationResult] = useState<CalculationResult | null>(null);
const [isCalculating, setIsCalculating] = useState(false);
// handleAutoCalculate 수정 (line 332-335)
const handleAutoCalculate = async () => {
const item = formData.items[activeItemIndex];
if (!item.openWidth || !item.openHeight) {
toast.error('오픈사이즈(W0, H0)를 입력해주세요.');
return;
}
setIsCalculating(true);
try {
const response = await quoteApi.calculate({
inputs: {
W0: parseFloat(item.openWidth),
H0: parseFloat(item.openHeight),
QTY: item.quantity,
INSTALL_TYPE: item.guideRailType,
CONTROL_TYPE: item.controller,
},
product_category: item.productCategory as 'screen' | 'steel' || 'screen',
});
if (response.success) {
setCalculationResult(response.data);
toast.success('자동 산출이 완료되었습니다.');
} else {
toast.error(response.message || '산출 중 오류가 발생했습니다.');
}
} catch (error) {
console.error('자동 산출 오류:', error);
toast.error('서버 연결에 실패했습니다.');
} finally {
setIsCalculating(false);
}
};
// 산출 결과 반영 함수 추가
const handleApplyCalculation = () => {
if (!calculationResult) return;
// 산출된 품목을 견적 항목에 반영
const newItems = calculationResult.items.map((item, index) => ({
id: `calc-${Date.now()}-${index}`,
floor: formData.items[activeItemIndex].floor,
code: item.item_code,
productCategory: formData.items[activeItemIndex].productCategory,
productName: item.item_name,
openWidth: formData.items[activeItemIndex].openWidth,
openHeight: formData.items[activeItemIndex].openHeight,
guideRailType: formData.items[activeItemIndex].guideRailType,
motorPower: formData.items[activeItemIndex].motorPower,
controller: formData.items[activeItemIndex].controller,
quantity: item.quantity,
wingSize: formData.items[activeItemIndex].wingSize,
inspectionFee: item.unit_price,
unitPrice: item.unit_price,
totalAmount: item.total_price,
}));
setFormData({
...formData,
items: [...formData.items.slice(0, activeItemIndex), ...newItems, ...formData.items.slice(activeItemIndex + 1)],
});
setCalculationResult(null);
toast.success(`${newItems.length}개 품목이 반영되었습니다.`);
};
```
#### 2.3 산출 결과 표시 UI 추가
```tsx
{/* 자동 견적 산출 버튼 아래에 추가 */}
{calculationResult && (
<Card className="border-green-200 bg-green-50/50">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Calculator className="h-4 w-4" />
산출 결과
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 계산 변수 */}
<div className="grid grid-cols-4 gap-2 text-sm">
{Object.entries(calculationResult.outputs).map(([key, val]) => (
<div key={key} className="bg-white p-2 rounded border">
<div className="text-gray-500 text-xs">{val.name}</div>
<div className="font-medium">{typeof val.value === 'number' ? val.value.toFixed(2) : val.value}</div>
</div>
))}
</div>
{/* 산출 품목 */}
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="p-2 text-left">품목코드</th>
<th className="p-2 text-left">품목명</th>
<th className="p-2 text-right">수량</th>
<th className="p-2 text-right">단가</th>
<th className="p-2 text-right">금액</th>
</tr>
</thead>
<tbody>
{calculationResult.items.map((item, i) => (
<tr key={i} className="border-b">
<td className="p-2 font-mono text-xs">{item.item_code}</td>
<td className="p-2">{item.item_name}</td>
<td className="p-2 text-right">{item.quantity}</td>
<td className="p-2 text-right">{item.unit_price.toLocaleString()}</td>
<td className="p-2 text-right font-medium">{item.total_price.toLocaleString()}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="bg-gray-50 font-medium">
<td colSpan={4} className="p-2 text-right">합계</td>
<td className="p-2 text-right">{calculationResult.costs.subtotal.toLocaleString()}</td>
</tr>
</tfoot>
</table>
{/* 반영 버튼 */}
<Button onClick={handleApplyCalculation} className="w-full bg-green-600 hover:bg-green-700">
<Check className="h-4 w-4 mr-2" />
품목에 반영하기
</Button>
</CardContent>
</Card>
)}
```
---
### Phase 3: 통합 테스트 (1일)
#### 3.1 테스트 시나리오
| 번호 | 테스트 케이스 | 입력값 | 예상 결과 |
|-----|-------------|-------|----------|
| 1 | 기본 스크린 산출 | W0=3000, H0=2500 | 가이드레일 PT-GR-3000, 모터 PT-MOTOR-150 |
| 2 | 대형 스크린 산출 | W0=5000, H0=4000 | 모터 규격 상향 (300K 이상) |
| 3 | 철재 산출 | W0=2000, H0=2000, steel | 중량 M*25 적용 |
| 4 | 품목 반영 | 산출 후 반영 클릭 | 견적 항목에 추가됨 |
| 5 | 에러 처리 | W0/H0 미입력 | "오픈사이즈를 입력해주세요" |
#### 3.2 검증 체크리스트
```
□ MNG 시뮬레이터에서 수식 계산 정확도 확인
□ React 자동산출 버튼 클릭 → API 호출 확인
□ 산출 결과 테이블 정상 표시
□ "품목에 반영하기" 클릭 → 견적 항목 추가 확인
□ 견적 저장 시 calculation_inputs 필드 저장 확인
□ 에러 시 적절한 메시지 표시
```
---
## 3. SAM 개발 규칙 요약
### 3.1 API 개발 규칙 (CLAUDE.md 참조)
```php
// Controller: FormRequest + ApiResponse 패턴
public function calculate(QuoteCalculateRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return $this->calculationService->calculate($request->validated());
}, __('message.quote.calculated'));
}
// Service: 비즈니스 로직 분리
class QuoteCalculationService extends Service
{
public function calculate(array $inputs, ?string $productCategory = null): array
{
$tenantId = $this->tenantId(); // 필수
// ...
}
}
// 응답 형식
{
"success": true,
"message": "견적이 산출되었습니다.",
"data": { ... }
}
```
### 3.2 React 개발 패턴
```typescript
// API 클라이언트 패턴 (react/src/lib/api/client.ts)
class ApiClient {
async post<T>(endpoint: string, data?: unknown): Promise<T>
async get<T>(endpoint: string): Promise<T>
}
// 컴포넌트 패턴
// - shadcn/ui 컴포넌트 사용
// - toast (sonner) 알림
// - FormField, Card, Button 등
```
### 3.3 MNG 개발 패턴
```php
// Artisan 명령어 패턴
protected $signature = 'quote:seed-formulas {--tenant=1}';
// 모델 사용
use App\Models\Quote\QuoteFormula;
use App\Models\Quote\QuoteFormulaCategory;
// 서비스 패턴
class QuoteFormulaService {
public function __construct(
private FormulaEvaluatorService $evaluator
) {}
}
```
---
## 4. 파일 구조
```
SAM/
├── mng/
│ ├── app/Console/Commands/
│ │ └── SeedQuoteFormulasCommand.php # 🆕 Phase 1
│ ├── app/Models/Quote/
│ │ ├── QuoteFormula.php # ✅ 있음
│ │ ├── QuoteFormulaCategory.php # ✅ 있음
│ │ └── QuoteFormulaRange.php # ✅ 있음
│ └── app/Services/Quote/
│ └── FormulaEvaluatorService.php # ✅ 있음
├── api/
│ ├── app/Http/Controllers/Api/V1/
│ │ └── QuoteController.php # ✅ calculate() 있음
│ ├── app/Services/Quote/
│ │ ├── QuoteCalculationService.php # ✅ 있음
│ │ └── FormulaEvaluatorService.php # ✅ 있음
│ └── database/seeders/
│ └── QuoteFormulaSeeder.php # 참조용 데이터
├── react/
│ ├── src/lib/api/
│ │ ├── client.ts # ✅ ApiClient 클래스
│ │ └── quote.ts # 🆕 Phase 2
│ └── src/components/quotes/
│ └── QuoteRegistration.tsx # ⚡ Phase 2 수정
└── docs/plans/
└── quote-auto-calculation-development-plan.md # 이 문서
```
---
## 5. 수식 계산 예시
```
입력: W0=3000mm, H0=2500mm, product_category=screen
계산 순서:
1. W1 = W0 + 140 = 3140mm (스크린 제작 가로)
2. H1 = H0 + 350 = 2850mm (스크린 제작 세로)
3. M = W1 * H1 / 1000000 = 8.949㎡ (면적)
4. K = M * 2 + W0 / 1000 * 14.17 = 60.41kg (중량)
5. G = H0 + 250 = 2750mm (가이드레일 길이)
6. S = W0 + 220 = 3220mm (케이스 사이즈)
범위 자동 선택:
- 가이드레일: G=2750 → 2438 < G ≤ 3000 → PT-GR-3000 × 2개
- 케이스: S=3220 → 3000 < S ≤ 3600 → PT-CASE-3600 × 1개
- 모터: K=60.41 → 0 < K ≤ 150 → PT-MOTOR-150 × 1개
```
---
## 6. 일정 요약
| Phase | 작업 | 예상 기간 | 상태 |
|-------|------|----------|------|
| 1 | MNG 시더 명령어 생성 | 1일 | ✅ 완료 |
| 2 | React Quote API 클라이언트 생성 | 0.5일 | ✅ 완료 |
| 3 | React handleAutoCalculate API 연동 | 0.5일 | ✅ 완료 |
| 4 | 산출 결과 UI 추가 | 0.5일 | ✅ 완료 |
| 5 | 문서 업데이트 | 0.5시간 | ✅ 완료 |
| **합계** | | **약 2시간** | ✅ |
---
## 7. 완료된 구현 내역
### 생성된 파일
| 파일 경로 | 역할 |
|----------|------|
| `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | MNG 견적수식 시더 명령어 |
| `react/src/lib/api/quote.ts` | React Quote API 클라이언트 |
### 수정된 파일
| 파일 경로 | 변경 내용 |
|----------|----------|
| `react/src/components/quotes/QuoteRegistration.tsx` | handleAutoCalculate API 연동, 산출 결과 UI 추가 |
### MNG 시더 실행 결과
```
✅ 견적수식 시드 완료!
카테고리: 11개
수식: 18개
범위: 18개
```
### React 기능 구현
- `handleAutoCalculate`: API 호출 및 로딩 상태 관리
- `handleApplyCalculation`: 산출 결과를 견적 항목에 반영
- 산출 결과 UI: 변수, 품목 테이블, 비용 합계 표시
- 에러 처리: 입력값 검증, API 에러 토스트
---
*문서 버전*: 3.0 (구현 완료)
*작성자*: Claude Code
*최종 업데이트*: 2025-12-22

View File

@@ -47,19 +47,19 @@
├─────────────────────────────────────────────────────────────────┤
│ - 도구: https://mng.sam.kr/dev-tools/flow-tester │
│ - 목적: 각 기능의 모든 경우의 수(TC)를 자동화 테스트 │
│ - 저장: api/docs/flow-tests/{feature}-flow.json
│ - 저장: docs/plans/flow-tests/{feature}-flow.json │
└─────────────────────────────────────────────────────────────────┘
```
**Flow Test JSON 형식:**
```json
{
"name": "기능명 플로우 테스트",
"description": "테스트 설명",
"name": "플로우 이름 (50자 이내)",
"description": "플로우 상세 설명",
"version": "1.0",
"config": {
"baseUrl": "https://api.sam.kr/api/v1",
"apiKey": "{{$env.FLOW_TESTER_API_KEY}}",
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
@@ -69,22 +69,69 @@
},
"steps": [
{
"id": "step_id",
"name": "단계명",
"method": "GET|POST|PUT|PATCH|DELETE",
"endpoint": "/endpoint",
"headers": { "Authorization": "Bearer {{login.token}}" },
"body": { },
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/api/v1/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": { "$.access_token": "@isString" }
},
"extract": { "token": "$.access_token" }
},
{
"id": "step2",
"name": "다음 단계",
"method": "GET",
"endpoint": "/api/v1/path",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": { "$.success": true }
},
"extract": { "key": "$.data.value" }
}
}
]
}
```
#### 🔴 Flow Test JSON 작성 규칙 (필수 준수)
| # | 규칙 | 설명 |
|---|------|------|
| 1 | **config.baseUrl** | 빈 문자열 `""` 로 설정 (서버에서 .env 기본값 사용) |
| 2 | **config.apiKey** | ❌ JSON에 포함하지 않음 (서버에서 자동 주입) |
| 3 | **variables** | `{{$env.XXX}}` 형식으로 환경변수 참조 |
| 4 | **steps[].body** | `{{변수명}}` 형식으로 variables 값 참조 |
| 5 | **headers.Authorization** | `Bearer {{login.token}}` 형식으로 추출한 토큰 사용 |
| 6 | **extract** | 다음 스텝에서 `{{stepId.변수명}}`으로 참조됨 |
| 7 | **endpoint** | `/api/v1`을 포함한 전체 경로 필수 |
| 8 | **flows 배열** | ❌ 사용 금지 - 최상위에 바로 `steps` 배열 사용 |
| 9 | **dependsOn** | ❌ 사용 금지 - steps 순서대로 실행됨 |
#### 변수 참조 규칙
```
┌─────────────────────────────────────────────────────────────────────────┐
│ ✅ 올바른 참조 │
├─────────────────────────────────────────────────────────────────────────┤
│ variables 참조: "user_id": "{{user_id}}" │
│ 환경변수 참조: "user_id": "{{$env.FLOW_TESTER_USER_ID}}" │
│ 추출값 참조: "Authorization": "Bearer {{login.token}}" │
│ 동적 값: "email": "test_{{$timestamp}}@example.com" │
├─────────────────────────────────────────────────────────────────────────┤
│ ❌ 잘못된 참조 │
├─────────────────────────────────────────────────────────────────────────┤
│ ❌ "user_id": "{{variables.user_id}}" (variables. 접두사 사용 금지) │
│ ❌ "Authorization": "Bearer {{login.accessToken}}" (추출 키 불일치) │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 2. 대상 페이지 목록
@@ -93,13 +140,13 @@
| # | React URL (예상) | 기능 | 관련 API | 연동 | Flow Test |
|---|-----------------|------|----------|:----:|:---------:|
| 5.1-1 | `/hr/employees/invite` | 사용자 초대 | `POST /v1/users/invite` | ✅ | |
| 5.1-1 | `/hr/employees/invite` | 사용자 초대 | `POST /v1/users/invite` | ✅ | |
| 5.1-2 | `/hr/employees/invitations` | 초대 목록 | `GET /v1/users/invitations` | ⏭️ | ⏭️ |
| 5.2-1 | `/settings/notifications` | 알림 설정 | `GET /v1/settings/notifications` | ✅ | |
| 5.2-2 | `/settings/notifications` | 알림 설정 수정 | `PUT /v1/settings/notifications` | ✅ | |
| 5.3-1 | `/account` | 회원 탈퇴 | `POST /v1/account/withdraw` | ✅ | |
| 5.3-2 | `/account` | 사용 중지 | `POST /v1/account/suspend` | ✅ | |
| 5.4-1 | `/accounting/sales/{id}` | 거래명세서 | `GET /v1/sales/{id}/statement` | ✅ | |
| 5.2-1 | `/settings/notifications` | 알림 설정 | `GET /v1/settings/notifications` | ✅ | |
| 5.2-2 | `/settings/notifications` | 알림 설정 수정 | `PUT /v1/settings/notifications` | ✅ | |
| 5.3-1 | `/account` | 회원 탈퇴 | `POST /v1/account/withdraw` | ✅ | |
| 5.3-2 | `/account` | 사용 중지 | `POST /v1/account/suspend` | ✅ | |
| 5.4-1 | `/accounting/sales/{id}` | 거래명세서 | `GET /v1/sales/{id}/statement` | ✅ | |
**Phase 5 Flow Test 파일:**
- `user-invitation-flow.json` - 사용자 초대 전체 플로우
@@ -111,13 +158,13 @@
| # | React URL (예상) | 기능 | 관련 API | 연동 | Flow Test |
|---|-----------------|------|----------|:----:|:---------:|
| 6.1-1 | `/accounting/bad-debts` | 악성채권 목록 | `GET /v1/bad-debts` | ✅ | |
| 6.1-2 | `/accounting/bad-debts` | 악성채권 요약 | `GET /v1/bad-debts/summary` | ✅ | |
| 6.1-3 | `/accounting/bad-debts/{id}` | 악성채권 상세 | `GET /v1/bad-debts/{id}` | ✅ | |
| 6.1-4 | `/accounting/bad-debts/new` | 악성채권 등록 | `POST /v1/bad-debts` | ✅ | |
| 6.2-1 | `/settings/popups` | 팝업 목록 | `GET /v1/popups` | ✅ | |
| 6.2-2 | `/settings/popups` | 활성 팝업 | `GET /v1/popups/active` | ✅ | |
| 6.2-3 | `/settings/popups/{id}` | 팝업 상세 | `GET /v1/popups/{id}` | ✅ | |
| 6.1-1 | `/accounting/bad-debts` | 악성채권 목록 | `GET /v1/bad-debts` | ✅ | |
| 6.1-2 | `/accounting/bad-debts` | 악성채권 요약 | `GET /v1/bad-debts/summary` | ✅ | |
| 6.1-3 | `/accounting/bad-debts/{id}` | 악성채권 상세 | `GET /v1/bad-debts/{id}` | ✅ | |
| 6.1-4 | `/accounting/bad-debts/new` | 악성채권 등록 | `POST /v1/bad-debts` | ✅ | |
| 6.2-1 | `/settings/popups` | 팝업 목록 | `GET /v1/popups` | ✅ | |
| 6.2-2 | `/settings/popups` | 활성 팝업 | `GET /v1/popups/active` | ✅ | |
| 6.2-3 | `/settings/popups/{id}` | 팝업 상세 | `GET /v1/popups/{id}` | ✅ | |
**Phase 6 Flow Test 파일:**
- `bad-debt-flow.json` - 악성채권 추심관리 CRUD 플로우
@@ -143,14 +190,14 @@
| # | React URL (예상) | 기능 | 관련 API | 연동 | Flow Test |
|---|-----------------|------|----------|:----:|:---------:|
| 8.1-1 | `/subscription` | 현재 구독 정보 | `GET /v1/subscriptions/current` | ✅ | |
| 8.1-2 | `/subscription` | 사용량 조회 | `GET /v1/subscriptions/usage` | ✅ | |
| 8.1-3 | `/subscription/export` | 자료 내보내기 | `POST /v1/subscriptions/export` | ✅ | |
| 8.1-4 | `/subscription` | 서비스 해지 | `POST /v1/subscriptions/{id}/cancel` | ✅ | |
| 8.2-1 | `/payments` | 결제 내역 | `GET /v1/payments` | ✅ | |
| 8.2-2 | `/payments/{id}` | 결제 상세 | `GET /v1/payments/{id}` | ✅ | |
| 8.2-3 | `/payments/{id}/statement` | 거래명세서 | `GET /v1/payments/{id}/statement` | ✅ | |
| 8.3-1 | `/companies/add` | 회사 추가 | `POST /v1/companies/request` | ✅ | |
| 8.1-1 | `/subscription` | 현재 구독 정보 | `GET /v1/subscriptions/current` | ✅ | |
| 8.1-2 | `/subscription` | 사용량 조회 | `GET /v1/subscriptions/usage` | ✅ | |
| 8.1-3 | `/subscription/export` | 자료 내보내기 | `POST /v1/subscriptions/export` | ✅ | |
| 8.1-4 | `/subscription` | 서비스 해지 | `POST /v1/subscriptions/{id}/cancel` | ✅ | |
| 8.2-1 | `/payments` | 결제 내역 | `GET /v1/payments` | ✅ | |
| 8.2-2 | `/payments/{id}` | 결제 상세 | `GET /v1/payments/{id}` | ✅ | |
| 8.2-3 | `/payments/{id}/statement` | 거래명세서 | `GET /v1/payments/{id}/statement` | ✅ | |
| 8.3-1 | `/companies/add` | 회사 추가 | `POST /v1/companies/request` | ✅ | |
**Phase 8 Flow Test 파일:**
- `subscription-flow.json` - 구독관리 플로우
@@ -193,7 +240,7 @@ Step 4: 수정 사항 정리
Step 5: Flow Test 작성
├── 해당 기능의 모든 TC(Test Case) 도출
├── Flow Test JSON 작성
├── api/docs/flow-tests/{feature}-flow.json 저장
├── docs/plans/flow-tests/{feature}-flow.json 저장
└── https://mng.sam.kr/dev-tools/flow-tester 에서 실행
```
@@ -326,16 +373,16 @@ Step 5: Flow Test 작성
**Flow Test 저장 위치:**
```
api/docs/flow-tests/
├── user-invitation-flow.json
├── notification-settings-flow.json
├── bad-debt-flow.json
├── popup-flow.json
├── board-management-flow.json
├── post-crud-flow.json
├── subscription-flow.json
├── payment-flow.json
└── company-request-flow.json
docs/plans/flow-tests/
├── user-invitation-flow.json ✅ 완료 (6 steps)
├── notification-settings-flow.json ✅ 완료 (10 steps)
├── account-management-flow.json ✅ 완료 (6 steps)
├── sales-statement-flow.json ✅ 완료 (9 steps)
├── bad-debt-flow.json ✅ 완료 (11 steps)
├── popup-flow.json ✅ 완료 (9 steps)
├── subscription-flow.json ✅ 완료 (13 steps)
├── payment-flow.json ✅ 완료 (12 steps)
└── company-request-flow.json ✅ 완료 (9 steps)
```
### 3.3 분석 템플릿
@@ -726,6 +773,18 @@ if (checkResult.valid) {
| 날짜 | 페이지 | 변경 내용 | 파일 | 승인 |
|------|--------|----------|------|------|
| 2025-12-22 | 8.3 | 회사 가입 신청 Flow Test 작성 (9 steps) | `company-request-flow.json` | ✅ |
| 2025-12-22 | 8.2 | 결제 관리 Flow Test 작성 (12 steps) | `payment-flow.json` | ✅ |
| 2025-12-22 | 8.1 | 구독 관리 Flow Test 작성 (13 steps) | `subscription-flow.json` | ✅ |
| 2025-12-22 | 6.2 | 팝업 관리 Flow Test 작성 (9 steps) | `popup-flow.json` | ✅ |
| 2025-12-22 | 6.1 | 부실채권 관리 Flow Test 작성 (11 steps) | `bad-debt-flow.json` | ✅ |
| 2025-12-22 | 5.4 | 매출 명세서 Flow Test 작성 (9 steps) | `sales-statement-flow.json` | ✅ |
| 2025-12-22 | 5.3 | 계정 관리 Flow Test 작성 (6 steps) | `account-management-flow.json` | ✅ |
| 2025-12-22 | 5.2 | 알림 설정 Flow Test 작성 (10 steps) | `notification-settings-flow.json` | ✅ |
| 2025-12-22 | - | Flow Test JSON 작성 규칙 정리 (9개 필수 규칙 + 변수 참조 규칙) | 문서 | ✅ |
| 2025-12-22 | - | endpoint 정책 추가 (/api/v1 전체 경로 필수) | 문서 + user-invitation-flow.json | ✅ |
| 2025-12-22 | - | Flow Test 파일 통합 (docs/plans/flow-tests/로 이동) | 9개 파일 이동 | ✅ |
| 2025-12-22 | 5.1-1 | 사용자 초대 Flow Test 작성 (14 flows, P1~P3) | `user-invitation-flow.json` | ✅ |
| 2025-12-22 | 8.1 | 구독관리 API 분석 - 완전 구현됨, 필드명 매핑 필요 | - | ✅ |
| 2025-12-22 | 8.2 | 결제내역 API 분석 - 완전 구현됨, 필드명 매핑 필요 | - | ✅ |
| 2025-12-22 | 8.3 | 회사 추가 API 분석 - 완전 구현됨, React 연동만 필요 | - | ✅ |
@@ -750,4 +809,4 @@ if (checkResult.valid) {
- **API Swagger UI**: http://sam.kr/api-docs/index.html
- **React 개발 서버**: http://dev.sam.kr
- **Flow Tester**: https://mng.sam.kr/dev-tools/flow-tester
- **Flow Test 예제**: `api/claudedocs/flow-tester-*.json`
- **Flow Test 파일**: `docs/plans/flow-tests/*.json`

View File

@@ -72,22 +72,22 @@ ListClient.tsx (클라이언트)
| # | 페이지 | React 경로 | API 엔드포인트 | 상태 |
|---|--------|-----------|---------------|------|
| A-1 | 악성채권 관리 | `/accounting/bad-debt-collection` | `GET/POST /v1/bad-debts` | ✅ 완료 |
| A-2 | 팝업 관리 | `/settings/popup-management` | `GET/POST /v1/popups` | ⏳ 대기 |
| A-3 | 결제 내역 | `/settings/payment-history` | `GET /v1/payments` | ⏳ 대기 |
| A-4 | 구독 관리 | `/settings/subscription` | `GET /v1/subscriptions` | ⏳ 대기 |
| A-5 | 알림 설정 | `/settings/notifications` | `GET/PUT /v1/settings/notifications` | ⏳ 대기 |
| A-6 | 거래처 원장 | `/accounting/vendor-ledger` | `GET /v1/vendor-ledger` | ⏳ 대기 |
| A-2 | 팝업 관리 | `/settings/popup-management` | `GET/POST /v1/popups` | ✅ 완료 |
| A-3 | 결제 내역 | `/settings/payment-history` | `GET /v1/payments` | ✅ 완료 |
| A-4 | 구독 관리 | `/settings/subscription` | `GET /v1/subscriptions` | ✅ 완료 |
| A-5 | 알림 설정 | `/settings/notifications` | `GET/PUT /v1/settings/notifications` | ✅ 완료 |
| A-6 | 거래처 원장 | `/accounting/vendor-ledger` | `GET /v1/vendor-ledger` | ⏭️ API 미존재 |
### 2.2 Phase B: 핵심 업무 기능
| # | 페이지 | React 경로 | API 엔드포인트 | 상태 |
|---|--------|-----------|---------------|------|
| B-1 | 매출 관리 | `/accounting/sales` | `GET/POST /v1/sales` | ⏳ 대기 |
| B-2 | 매입 관리 | `/accounting/purchase` | `GET/POST /v1/purchases` | ⏳ 대기 |
| B-3 | 입금 관리 | `/accounting/deposit` | `GET/POST /v1/deposits` | ⏳ 대기 |
| B-4 | 출금 관리 | `/accounting/withdrawal` | `GET/POST /v1/withdrawals` | ⏳ 대기 |
| B-5 | 거래처 관리 | `/accounting/vendor` | `GET/POST /v1/clients` | ⏳ 대기 |
| B-6 | 어음 관리 | `/accounting/bill` | `GET/POST /v1/bills` | ⏳ 대기 |
| B-1 | 매출 관리 | `/accounting/sales` | `GET/POST /v1/sales` | ✅ 완료 (기존 연동) |
| B-2 | 매입 관리 | `/accounting/purchase` | `GET/POST /v1/purchases` | ✅ 완료 (기존 연동) |
| B-3 | 입금 관리 | `/accounting/deposit` | `GET/POST /v1/deposits` | ✅ 완료 |
| B-4 | 출금 관리 | `/accounting/withdrawal` | `GET/POST /v1/withdrawals` | ✅ 완료 |
| B-5 | 거래처 관리 | `/accounting/vendor` | `GET/POST /v1/clients` | ✅ 완료 |
| B-6 | 어음 관리 | `/accounting/bills` | `GET/POST /v1/bills` | ✅ 완료 |
### 2.3 Phase C: 인사/근태
@@ -776,10 +776,11 @@ export function transformFrontendToApi(data: Record<string, any>): Record<string
| 일차 | 작업 내용 | 상태 |
|------|----------|------|
| Day 1 | A-1 악성채권 관리 연동 | ✅ 완료 (2025-12-23) |
| Day 2 | A-2 팝업 관리 연동 | |
| Day 3 | A-3 결제 내역, A-4 구독 관리 연동 | ⏳ |
| Day 4 | A-5 알림 설정 연동 | ⏳ |
| Day 5 | A-6 거래처 원장 연동 + 테스트 | ⏳ |
| Day 2 | A-2 팝업 관리 연동 | ✅ 완료 (2025-12-23) |
| Day 3 | A-3 결제 내역 연동 | ✅ 완료 (2025-12-23) |
| Day 4 | A-4 구독 관리 연동 | ✅ 완료 (2025-12-23) |
| Day 5 | A-5 알림 설정 연동 | ✅ 완료 (2025-12-23) |
| Day 6 | A-6 거래처 원장 - API 미존재로 건너뜀 | ⏭️ 건너뜀 |
### Phase B: 핵심 업무 기능 (2주)
@@ -800,6 +801,16 @@ export function transformFrontendToApi(data: Record<string, any>): Record<string
| 2025-12-23 | 문서 초안 작성 | Claude |
| 2025-12-23 | Phase B 상세 필드 매핑 추가 (6.1~6.6) | Claude |
| 2025-12-23 | A-1 악성채권 관리 API 연동 완료 (`actions.ts`, `page.tsx`, `index.tsx`) | Claude |
| 2025-12-23 | A-2 팝업 관리 API 연동 완료 (`actions.ts`, `page.tsx`, `PopupList.tsx`, `[id]/page.tsx`, `[id]/edit/page.tsx`) | Claude |
| 2025-12-23 | A-3 결제 내역 API 연동 완료 (`types.ts`, `actions.ts`, `page.tsx`, `PaymentHistoryClient.tsx`, `index.ts`) | Claude |
| 2025-12-23 | A-4 구독 관리 API 연동 완료 (`types.ts`, `utils.ts`, `actions.ts`, `page.tsx`, `SubscriptionClient.tsx`, `index.ts`) | Claude |
| 2025-12-23 | A-5 알림 설정 API 연동 완료 (`types.ts`, `actions.ts`, `page.tsx`, `NotificationSettingsClient.tsx`, `index.ts`) | Claude |
| 2025-12-23 | A-6 거래처 원장 - API 미존재 확인, Phase A 완료 | Claude |
| 2025-12-23 | B-1 매출 관리 - 기존 API 연동 확인 (`/api/proxy/sales` 사용) | Claude |
| 2025-12-23 | B-2 매입 관리 - 기존 API 연동 확인 (`/api/proxy/purchases` 사용) | Claude |
| 2025-12-23 | B-3 입금 관리 API 연동 완료 (`types.ts`: API 타입 추가, `index.tsx`: Mock → API 호출 전환) | Claude |
| 2025-12-23 | B-4 출금 관리 API 연동 완료 (`types.ts`: API 타입 추가, `index.tsx`: Mock → API 호출 전환) | Claude |
| 2025-12-23 | B-5 거래처 관리 API 연동 완료 (`types.ts`: API 타입 추가, `actions.ts`: Server Actions, `page.tsx`: 서버 컴포넌트, `VendorManagementClient.tsx`: 클라이언트 컴포넌트) | Claude |
---

223
projects/index_projects.md Normal file
View File

@@ -0,0 +1,223 @@
# 프로젝트 문서 인덱스
> SAM 시스템 개발 프로젝트별 문서 모음
> **최종 업데이트**: 2025-12-23
---
## 프로젝트 현황 요약
| 프로젝트 | 상태 | 설명 |
|---------|------|------|
| [mes](#mes---meserp-프로젝트) | 🟡 진행중 | 차세대 MES/ERP 기능 개발 |
| [quotation](#quotation---견적-기능) | 🟢 Phase 3 완료 | 5130 견적 → SAM 이관 |
| [api-integration](#api-integration---react--api-연동) | 🟡 진행중 | React ↔ API 연동 |
| [5130-migration](#5130-migration---품목-마이그레이션) | 🟡 Phase 1 진행중 | 5130 품목 데이터 마이그레이션 |
| [legacy-5130](#legacy-5130---레거시-분석) | 📚 참조용 | 5130 레거시 모듈 분석 |
| [mng-mobile-responsive](#mng-mobile-responsive---모바일-반응형) | 🟡 진행중 | mng 모바일 반응형 개선 |
| [auto-login](#auto-login---자동-로그인) | ⚪ 대기 | 자동 로그인 기능 |
| [migration-5130-mng](#migration-5130-mng---5130--mng-마이그레이션) | 🟡 진행중 | 5130 → mng 통합 마이그레이션 |
---
## 프로젝트 상세
### mes - MES/ERP 프로젝트
**경로**: `docs/projects/mes/`
**상태**: 🟡 Phase 0 (베이스라인 분석) 30% 완료
**목표**: SAM 시스템의 차세대 MES/ERP 기능 개발
**핵심 문서**:
- [README.md](./mes/README.md) - 프로젝트 개요 및 문서 안내
- [MES_PROGRESS_TRACKER.md](./mes/MES_PROGRESS_TRACKER.md) - 진행 상황 추적
- [MES_PROJECT_ROADMAP.md](./mes/MES_PROJECT_ROADMAP.md) - 전체 로드맵
**분석 결과**:
- `00_baseline/` - Phase 0 분석 결과
- [PHASE_0_FINAL_REPORT.md](./mes/00_baseline/PHASE_0_FINAL_REPORT.md)
- [BACKEND_DEVELOPMENT_ROADMAP_V2.md](./mes/00_baseline/BACKEND_DEVELOPMENT_ROADMAP_V2.md)
- `docs_breakdown/` - 문서 분석 (7개)
**v2 분석**:
- `v2-analysis/` - MES v2 화면 분석
- `quote-analysis/` - 견적 분석
- `order-analysis/` - 주문 분석
- `production-analysis/` - 생산 분석
- `customer-analysis/` - 거래처 분석
- `site-analysis/` - 현장 분석
- `price-analysis/` - 단가 분석
- `master-data-analysis/` - 기준정보 분석
- `production-userflow/` - 생산 유저플로우
---
### quotation - 견적 기능
**경로**: `docs/projects/quotation/`
**상태**: 🟢 Phase 3 완료 (2025-12-19)
**목표**: 5130 레거시 견적 기능을 SAM 시스템으로 이관
**핵심 문서**:
- [MASTER_PLAN.md](./quotation/MASTER_PLAN.md) - 마스터 플랜
- [PROGRESS.md](./quotation/PROGRESS.md) - 진행 현황
**Phase 문서**:
| Phase | 상태 | 경로 |
|-------|------|------|
| 1. 5130 분석 | ✅ 완료 | `phase-1-5130-analysis/` |
| 2. mng 분석 | ✅ 완료 | `phase-2-mng-analysis/` |
| 3. 구현 | ✅ 완료 | `phase-3-implementation/` |
| 4. API 개발 | ⚪ 대기 | `phase-4-api/` |
**참조 자료**:
- `screenshots/` - MES 프로토타입 화면 캡쳐 (7개)
---
### api-integration - React ↔ API 연동
**경로**: `docs/projects/api-integration/`
**상태**: 🟡 Phase 4 진행중
**목표**: React(dev.sam.kr)와 API(api.sam.kr) 완벽 연동
**핵심 문서**:
- [MASTER_PLAN.md](./api-integration/MASTER_PLAN.md) - 마스터 플랜
- [PROGRESS.md](./api-integration/PROGRESS.md) - 진행 현황
- [WORKFLOW.md](./api-integration/WORKFLOW.md) - 작업 프로세스
**Phase 문서**:
| Phase | 상태 | 경로 |
|-------|------|------|
| 1. 테이블 통합 | 🟢 완료(스킵) | `phase-1-table-migration/` |
| 2. 메뉴 추출 | 🟡 진행중 | `phase-2-menu-extraction/` |
| 3. API 매핑 | 🟡 진행중 | `phase-3-api-mapping/` |
| 4. 연동+검증 | 🟡 진행중 | `phase-4-integration/` |
**TC 파일**: `phase-4-integration/tc/` - 기능별 테스트 케이스 JSON (17개)
---
### 5130-migration - 품목 마이그레이션
**경로**: `docs/projects/5130-migration/`
**상태**: 🟡 Phase 1 진행중
**목표**: 5130 품목(부품, 자재, BOM) 데이터를 SAM DB로 이전
**핵심 문서**:
- [MASTER_PLAN.md](./5130-migration/MASTER_PLAN.md) - 마스터 플랜
- [PROGRESS.md](./5130-migration/PROGRESS.md) - 진행 현황
**Phase 문서**:
| Phase | 상태 | 경로 |
|-------|------|------|
| 1. 소스 분석 | 🟡 진행중 | `phase-1-source-analysis/` |
| 2. 타겟 분석 | ⚪ 대기 | `phase-2-target-analysis/` |
| 3. 매핑 설계 | ⚪ 대기 | `phase-3-mapping/` |
---
### legacy-5130 - 레거시 분석
**경로**: `docs/projects/legacy-5130/`
**상태**: 📚 참조용 문서
**용도**: 5130 레거시 시스템 모듈별 분석
**모듈별 분석 문서**:
| 문서 | 내용 |
|------|------|
| [00_OVERVIEW.md](./legacy-5130/00_OVERVIEW.md) | 시스템 개요 |
| [01_MATERIAL.md](./legacy-5130/01_MATERIAL.md) | 자재 관리 |
| [02_PRODUCT.md](./legacy-5130/02_PRODUCT.md) | 제품 관리 |
| [03_ESTIMATE.md](./legacy-5130/03_ESTIMATE.md) | 견적 관리 |
| [04_PRODUCTION.md](./legacy-5130/04_PRODUCTION.md) | 생산 관리 |
| [05_SHIPPING.md](./legacy-5130/05_SHIPPING.md) | 출하 관리 |
| [06_QUALITY.md](./legacy-5130/06_QUALITY.md) | 품질 관리 |
| [07_ACCOUNTING.md](./legacy-5130/07_ACCOUNTING.md) | 회계 관리 |
| [08_SAM_COMPARISON.md](./legacy-5130/08_SAM_COMPARISON.md) | SAM 비교 분석 |
| [draw-module.md](./legacy-5130/draw-module.md) | 도면 모듈 |
---
### mng-mobile-responsive - 모바일 반응형
**경로**: `docs/projects/mng-mobile-responsive/`
**상태**: 🟡 진행중
**목표**: mng 관리자 패널 모바일 반응형 개선
**문서**:
- [01-analysis.md](./mng-mobile-responsive/01-analysis.md) - 분석
- [02-implementation-plan.md](./mng-mobile-responsive/02-implementation-plan.md) - 구현 계획
- [06-excluded-menus.md](./mng-mobile-responsive/06-excluded-menus.md) - 제외 메뉴
- [PROGRESS.md](./mng-mobile-responsive/PROGRESS.md) - 진행 현황
---
### auto-login - 자동 로그인
**경로**: `docs/projects/auto-login/`
**상태**: ⚪ 대기
**목표**: 자동 로그인 기능 구현
**문서**:
- [PROGRESS.md](./auto-login/PROGRESS.md) - 진행 현황
---
### migration-5130-mng - 5130 → mng 마이그레이션
**경로**: `docs/projects/migration-5130-mng/`
**상태**: 🟡 진행중
**목표**: 5130 기능을 mng로 통합 마이그레이션
**문서**:
- [MIGRATION_TRACKER.md](./migration-5130-mng/MIGRATION_TRACKER.md) - 마이그레이션 추적
---
## 디렉토리 구조
```
docs/projects/
├── INDEX.md # 이 파일
├── mes/ # MES/ERP 프로젝트 (핵심)
│ ├── 00_baseline/ # Phase 0 분석
│ ├── v1-analysis/ # v1 분석
│ ├── v2-analysis/ # v2 화면 분석
│ └── phases/ # Phase별 진행
├── quotation/ # 견적 기능
│ ├── phase-1-5130-analysis/
│ ├── phase-2-mng-analysis/
│ ├── phase-3-implementation/
│ └── screenshots/
├── api-integration/ # React ↔ API 연동
│ ├── phase-1-table-migration/
│ ├── phase-2-menu-extraction/
│ ├── phase-3-api-mapping/
│ └── phase-4-integration/tc/
├── 5130-migration/ # 품목 마이그레이션
│ ├── phase-1-source-analysis/
│ ├── phase-2-target-analysis/
│ └── phase-3-mapping/
├── legacy-5130/ # 레거시 분석 (참조용)
├── mng-mobile-responsive/ # 모바일 반응형
├── auto-login/ # 자동 로그인
└── migration-5130-mng/ # 5130→mng 마이그레이션
```
---
## 관련 문서
- [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스
- [docs/plans/index_plans.md](../plans/index_plans.md) - 기획 문서 인덱스
- [docs/guides/PROJECT_DEVELOPMENT_POLICY.md](../guides/PROJECT_DEVELOPMENT_POLICY.md) - 공통 개발 정책
- [CURRENT_WORKS.md](../../CURRENT_WORKS.md) - 현재 작업
---
**범례**:
- 🟢 완료
- 🟡 진행중
- ⚪ 대기
- 📚 참조용

View File

@@ -0,0 +1,287 @@
# Claude Code 프롬프트 워크플로우 공유
> **목적:** 팀 내 Claude Code 사용 방식 공유 및 개선 논의
> **작성일:** 2025-12-23
> **작성자:** hskwon
---
## 📋 현재 사용 중인 워크플로우
### 1. Code-Workflow 스킬 (5단계 강제 프로세스)
코드 수정 시 체계적인 프로세스를 **강제**하는 커스텀 스킬입니다.
```
분석 → 수정 → 검증 → 정리 → 커밋
```
#### 왜 사용하는가?
| 문제 | 해결책 |
|------|--------|
| Claude가 분석 없이 바로 수정 시작 | 1단계에서 **분석 후 승인** 강제 |
| 여러 파일 동시 수정 → 롤백 어려움 | 한 번에 하나씩 순차 수정 |
| 수정 후 검증 생략 | 3단계 검증 필수 |
| 커밋 메시지 대충 작성 | 4단계에서 변경 내용 정리 문서화 |
| 자동 커밋으로 실수 | 5단계에서 **커밋 전 승인** 강제 |
#### 핵심 규칙
```markdown
🔴 필수 규칙 (반드시 준수)
1. 분석 후 승인 필수
- 분석 결과 + 일정 산정 제시
- AskUserQuestion으로 승인 요청
- 승인 없이 수정 진행 금지
2. 커밋 전 승인 필수
- 변경 내용 요약 제시
- 승인 없이 커밋 금지
- 승인 후 → 요약 MD 작성 → 커밋
```
#### 스킬 호출 방법
```bash
/code-workflow # 또는 Claude가 코드 수정 요청 시 자동 활성화
```
---
### 2. 프로젝트 문서 기반 진행 관리
#### 문서 구조
```
docs/projects/{프로젝트명}/
├── MASTER_PLAN.md # 마스터 플랜 (전체 로드맵)
├── PROGRESS.md # 진행 현황 (실시간 업데이트)
├── screenshots/ # 참조 이미지
├── phase-1-xxx/ # Phase별 상세 문서
│ ├── README.md # Phase 체크리스트 및 요약
│ └── *.md # 분석/구현 문서
├── phase-2-xxx/
└── phase-3-xxx/
```
#### 핵심 문서 역할
| 문서 | 역할 | 업데이트 주기 |
|------|------|---------------|
| `MASTER_PLAN.md` | 전체 방향, Phase 정의, 핵심 원칙 | 큰 변경 시 |
| `PROGRESS.md` | 현재 진행률, 완료 작업, 다음 작업 | **매 작업 완료 시** |
| `phase-*/README.md` | 각 Phase 체크리스트 | Phase 진행 중 |
#### 실제 사용 예시 (견적 프로젝트)
```markdown
# PROGRESS.md 예시
## 전체 진행률
| Phase | 상태 | 진행률 | 시작일 | 완료일 |
|-------|------|--------|--------|--------|
| Phase 1: 5130 분석 | ✅ 완료 | 100% | 2025-12-19 | 2025-12-19 |
| Phase 2: mng 분석 | ✅ 완료 | 100% | 2025-12-19 | 2025-12-19 |
| Phase 3: 구현 | ✅ 완료 | 100% | 2025-12-19 | 2025-12-19 |
| Phase 4: API 개발 | 🔄 진행 | 60% | 2025-12-19 | - |
## 🔄 현재 작업
**현재 Phase:** Phase 4 진행 중
**완료된 작업:** mng 패턴 적용 - DB 기반 견적 산출 서비스 재작성
**다음 작업:** 견적 API 통합 테스트 및 Swagger 문서화
```
---
### 3. WORKFLOW_STATE.md (작업 중 상태 추적)
코드 수정 중 **중단 후 재개**를 위한 임시 상태 파일입니다.
#### 생성 시점
- code-workflow 2단계(순차 수정) 시작 시 생성
- 5단계(커밋) 완료 후 삭제
#### 내용
```markdown
# Workflow 진행 상태
## 현재 단계: 2단계 (순차 수정)
## 전체 작업 목록
- [x] 1단계: 분석 완료
- [ ] 2단계: 순차 수정 (3/7 완료)
- [ ] 3단계: 검증 대기
- [ ] 4단계: 문서 정리 대기
- [ ] 5단계: 커밋 대기
## 순차 수정 상세 (2단계)
- [x] 수정 1/7: tenants 마이그레이션 생성
- [x] 수정 2/7: folders 마이그레이션 생성
- [x] 수정 3/7: files 마이그레이션 확장
- [ ] 수정 4/7: FileStorageService 구현 ← 현재 진행 중
- [ ] 수정 5/7: Tenant 모델 확장
- [ ] 수정 6/7: File 모델 확장
- [ ] 수정 7/7: Folder 모델 생성
```
#### 재개 시
```
사용자: "어디까지 진행했지?"
Claude: [WORKFLOW_STATE.md 확인]
📋 작업 진행 현황
- 현재 단계: 2단계 (순차 수정)
- 전체 진행률: 43% (3/7 완료)
- 마지막 완료: files 마이그레이션 확장
계속 진행할까요? (y/n)
```
---
### 4. 도구 사용 기록
모든 단계에서 **사용된 도구를 명시적으로 기록**합니다.
#### Footer 형식
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 사용된 도구
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 MCP 서버
- Sequential Thinking: 복잡한 의존성 분석
- Context7: Laravel Storage 공식 문서 조회
🧠 SuperClaude 페르소나
- backend-architect: 파일 저장 시스템 아키텍처 설계
- security-engineer: 파일 업로드 보안 검토
🛠️ 네이티브 도구
- Read: 5회 (File.php, Tenant.php 등)
- Glob: 3회
- Grep: 2회
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
---
## 💡 이 방식의 장점
### 1. 세션 중단에 강함
- `PROGRESS.md` + `WORKFLOW_STATE.md`로 언제든 이어서 작업 가능
- Claude 컨텍스트가 끊겨도 문서만 읽으면 상태 파악
### 2. 실수 방지
- 분석 → 승인 → 수정 순서 강제
- 커밋 전 승인 강제
- 여러 파일 동시 수정 금지
### 3. 문서화 자동화
- 변경 내용이 자동으로 `docs/changes/` 에 기록
- 나중에 "왜 이렇게 바꿨지?" 추적 가능
### 4. 복잡한 프로젝트 관리
- Phase별 분리로 큰 프로젝트도 관리 가능
- 체크리스트 기반으로 누락 방지
---
## ⚠️ 이 방식의 단점
### 1. 초기 설정 비용
- 스킬 파일, CLAUDE.md 설정 필요
- 프로젝트별 문서 구조 생성 필요
### 2. 간단한 작업에는 과함
- 오타 수정 같은 건 5단계가 불필요
- 예외 처리 규칙 필요 (hotfix 모드 등)
### 3. Claude 의존
- Claude가 규칙을 "잊어버리면" 무용지물
- 가끔 리마인드 필요
---
## 🤔 논의 포인트
### 1. 다른 분들은 어떻게 사용하고 계신가요?
- 프로젝트 문서 구조는?
- 코드 수정 시 어떤 프로세스?
- 세션 중단 후 재개 방법?
- 커밋 관리 방식?
### 2. 개선 아이디어
- 현재 방식에서 불편한 점?
- 더 좋은 문서 구조?
- 더 나은 진행 상태 추적 방법?
### 3. 공통화 가능한 부분
- 팀 공통 CLAUDE.md?
- 공통 스킬 라이브러리?
- 프로젝트 템플릿?
---
## 📁 관련 파일
| 파일 | 설명 |
|------|------|
| `.claude/skills/code-workflow/SKILL.md` | code-workflow 스킬 정의 |
| `CLAUDE.md` | 프로젝트 지시사항 |
| `docs/projects/*/PROGRESS.md` | 프로젝트별 진행 현황 |
| `docs/projects/*/MASTER_PLAN.md` | 프로젝트별 마스터 플랜 |
---
## 📝 스킬 전문 (참고용)
<details>
<summary>code-workflow 스킬 전체 내용 (클릭해서 펼치기)</summary>
```markdown
# 5단계 워크플로우 요약
## 1단계: 분석 (ANALYZE)
- SAM 프로젝트 컨텍스트 로드
- MCP로 관련 파일 검색 및 의존성 분석
- 영향받는 파일 식별
- 예상 부작용/리스크 파악
- **사용자 확인 후 다음 단계로**
## 2단계: 순차 수정 (MODIFY)
- WORKFLOW_STATE.md 생성
- 한 번에 하나씩 수정
- 각 수정 완료 후 상태 업데이트
- 이전 코드 주석 처리 (삭제 금지)
## 3단계: 검증 (VERIFY)
- 문법 오류 확인 (Pint)
- 로직 검증
- 기존 기능 유지 확인
- 보안/성능 체크
## 4단계: 변경 내용 정리 (SUMMARY)
- docs/changes/YYYYMMDD_HHMM_feature_name.md 생성
- CURRENT_WORKS.md 업데이트
## 5단계: Git 커밋 (COMMIT)
- 변경 파일 확인
- 커밋 메시지 제안
- **사용자 확인 후 커밋**
- WORKFLOW_STATE.md 삭제
```
</details>