kintex/_workspace/01_backend_contracts.md
zio 9987d958db feat(v2.0): 자동전시시스템 하네스 재구성 + PLANNING v2.0 + 백엔드 스캐폴드 + 평면도 자산
PLANNING v2.0: 자동전시시스템 재정의, 6역할 웹/모바일 분리, 도메인 M10~M18(옥션 M15·관람객 M10·마케팅/공개사이트 M12·BI M16(운영사 ROI 포함)·CMS M17·관리자 M18), WISE/UIWS 공통·시스템관리 레이어 §5B(2FA OTP), 옥션/견적서/역경매, M2/M3 3안 생성→선택/병합. design.md v1.1(Stitch 정합·SCR 매핑).

하네스 재구성: 전문 에이전트 17종(아키텍트 AA·SA·TA·DA·NA / 공통 common-dev / 코어 backend·frontend·db / 도메인 bidding·visitor·cms·bi·admin / AI ai-dev·visualizer / QA·devops) + kintex-impl-orchestrator v2.0(Phase A~E) + IMPLEMENTATION_BACKLOG v2.0 + CLAUDE.md.

백엔드 스캐폴드: Spring Boot 3.2.5(com.zioinfo.kintex) + JWT/RBAC + WebSocket + 버전형 룰엔진 + M2~M5 컨트롤러(M3 사전검증·M4 견적·M5 RenderJob 실구현, 공간경로 501 대기). compileJava SUCCESS.

자산: 평면도 JPG 15장 + 매니페스트(R4 트렌치·CAD 해소). CAD 93MB·build는 gitignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:32:40 +09:00

326 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 킨텍스 AI 전시관리 — P0 백엔드 API 계약 (D-3)
> 작성: kintex-backend-dev · 근거: `docs/PLANNING.md` v1.2(§5 M2~M5·§7 ERD·§8 아키텍처)·`docs/design.md` v1.0(SCR-01~12)·`docs/IMPLEMENTATION_BACKLOG.md`(S-1·D-3·C-1)
> 스택: Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS) + Redis + WebSocket(STOMP). 패키지 `com.zioinfo.kintex`.
> 이 문서는 **frontend·db-engineer·qa 대조용 단일 계약**이다. 스키마 필요 매퍼는 §8에 목록화(db-engineer 인수).
---
## 0. 공통 규약
### 0-1. 응답 봉투 (`ApiResponse<T>`)
```json
{ "success": true, "data": { ... }, "error": null }
{ "success": false, "data": null, "error": { "code": "FORBIDDEN", "message": "이 행사/부스에 대한 권한이 없습니다." } }
```
- 목록은 `PageResponse<T>` = `{ "items": [...], "page": 0, "size": 20, "total": 123 }` (현재 P0에서 목록은 배열 직접 반환도 허용 — 갤러리/워크스페이스).
### 0-2. 오류 코드 → HTTP
| code | HTTP | 의미 |
|---|---|---|
| `VALIDATION` | 400 | 요청 값 오류(필드 메시지 포함) |
| `UNAUTHORIZED` | 401 | 미인증/토큰 만료 |
| `FORBIDDEN` | 403 | 행사/부스 권한 없음 |
| `NOT_FOUND` | 404 | 대상 없음 |
| `CONFLICT` | 409 | 상태 충돌(낙관적 잠금 등) |
| `COMPLIANCE_BLOCKED` | 422 | 규정 위반(차단) |
| `RENDER_QUOTA_EXCEEDED` | 429 | 행사 이미지 생성 쿼터 소진 |
| `NOT_REGISTERED_COMPANY` | 403 | 미등록 장치업체 초대 차단 |
| `NOT_IMPLEMENTED` | 501 | 매퍼/엔진 구현 대기(스켈레톤) |
| `INTERNAL` | 500 | 서버 오류(요약만) |
### 0-3. 보안 불변 (계약 강제)
- **스택트레이스·내부 세부 미노출** — 응답 `error.message`는 사람이 읽을 요약만. 상세는 서버 로그.
- **ServerOut류 민감정보(IP·SSH·비밀번호·os_pw_enc·비밀번호 해시·내부 식별자) 응답 완전 제외.** 사용자/업체 표시는 이름·역할·번호 등 비민감 필드만.
- `GEMINI_API_KEY`는 백엔드에서 다루지 않는다(나노바나나 Python 워커 전용). M5는 큐 발행까지만.
- **AI 생성 이미지**는 응답에 `watermarkRequired:true` + `watermarkText` + `notice`(계약·심사 서류 사용 금지)를 **항상** 포함(제거 불가 — PLANNING §6-5).
### 0-4. 인증 헤더
- `Authorization: Bearer <JWT>` (HS256). JWT는 `sub`(userId)·`name`·`roles`(eventId→역할)·`hm`(홀매니저) 클레임.
- 공개 경로(인증 불필요): `GET /health`, `POST /api/auth/login`, `/ws/**`, `POST /api/internal/render/callback`(워커 토큰 인증).
### 0-5. 행사 단위 RBAC (역할)
`ORGANIZER`(주최자·owner) · `EXHIBITOR`(참가업체·부스 멤버) · `CONTRACTOR`(장치업체) · `HALL_MANAGER`(킨텍스 내부·전 행사 열람+승인).
- 모든 도메인 경로는 `{eventId}` 스코프. 가드: 열람=행사 멤버 or 홀매니저 / 편집·액션=역할별(각 엔드포인트 명시).
---
## 1. 헬스체크
### `GET /health` — 공개
```json
{ "success": true, "data": { "status": "UP", "service": "kintex-backend", "time": "2026-07-11T…" }, "error": null }
```
---
## 2. 인증·워크스페이스 (C-1 / SCR-01)
### `POST /api/auth/login` — 공개
요청:
```json
{ "email": "pm@expo.co.kr", "password": "••••••" }
```
응답 `LoginResponse`:
```json
{
"accessToken": "<jwt>",
"expiresInSeconds": 3600,
"user": { "userId": "u-1", "displayName": "김주최", "hallManager": false },
"workspaces": [
{ "eventId": "e-2026-smf", "eventName": "2026 스마트팩토리 코리아",
"startDate": "2026-08-11", "endDate": "2026-08-14",
"hallLabel": "제2전시장 홀7", "myRole": "ORGANIZER", "dday": 31 }
]
}
```
- 실패: `UNAUTHORIZED`(이메일/비밀번호 불일치, 메시지 일반화). **비밀번호 해시 등 미노출.**
### `GET /api/auth/workspaces` — 인증
응답: `WorkspaceDto[]` (위 workspaces 배열과 동일 shape).
### `GET /api/auth/me` — 인증
응답 `KintexPrincipal`: `{ "userId","displayName","eventRoles":{"e-…":"ORGANIZER"},"hallManager":false }`.
### `POST /api/auth/accept-invite` — 인증
요청:
```json
{ "inviteCode": "INV-8F2K", "companyRegistrationNo": "123-45-67890" }
```
- 장치업체(CONTRACTOR) 초대 수락 시 `companyRegistrationNo`**킨텍스 등록업체 검증** — 미등록이면 `NOT_REGISTERED_COMPANY`(403).
- 성공: 역할이 추가된 새 `LoginResponse`(신규 토큰 포함) 반환.
> 스켈레톤 현황: `login`·`workspaces`·`accept-invite`는 `UserMapper`(§8) 대기로 **501**. JWT 발급/검증·RBAC 가드·`/me`는 완성.
---
## 3. M2 플로어플랜 스튜디오 (SCR-03/04) — **P0**
베이스: `/api/events/{eventId}/halls/{hallId}/layout`
### `GET …/layout?version={n}` — 열람(멤버/홀매니저)
응답 `LayoutDto`:
```json
{
"layoutId": "lay-1", "eventId": "e-…", "hallId": "H7", "version": 3,
"name": "배치안 B", "status": "draft",
"booths": [
{ "boothId": "b-102", "boothNo": "A-102", "type": "independent",
"polygon": [[0,0],[6,0],[6,3],[0,3],[0,0]], "sizeM": [6,3],
"heightM": 4.2, "floorLoadTPerM2": 3.0, "assignedCompanyName": "(주)한빛로보틱스", "premium": false }
],
"summary": { "boothCount": 486, "targetBoothCount": 510, "salesAreaM2": 4374.0,
"minAisleWidthM": 3.2, "violationBlock": 0, "violationWarn": 3 },
"updatedAt": "2026-07-11T…"
}
```
- `polygon`: 홀 로컬 좌표계(미터), 닫힌 링. 서버가 PostGIS polygon으로 저장/검증.
### `PUT …/layout` — 편집(ORGANIZER)
요청 `LayoutSaveRequest`: `{ "name":"배치안 B", "version":3, "booths":[ BoothDto… ] }` (`booths` 비어있지 않음).
응답: 저장된 `LayoutDto`(저장 시 규정 검증이 `summary`에 반영).
### `POST …/layout/validate?version={n}` — 규정검증(ORGANIZER/HALL_MANAGER)
응답 `ComplianceReport`:
```json
{
"rulesetVersion": "compliance-v1.0",
"disclaimer": "본 룰셋은 사전 필터이며 최종 승인은 킨텍스 및 구조기술사의 판단에 따른다.",
"blockCount": 2, "warnCount": 3, "passCount": 6, "submittable": false,
"violations": [
{ "pin": 1, "code": "AISLE_WIDTH_MIN", "group": "egress",
"label": "피난 통로 폭 최소 3m", "severity": "block", "measured": "측정 2.8m" }
]
}
```
- `submittable=false`(차단 존재) → 프론트 제출 비활성(SCR-03/09). `pin`은 도면 하이라이트 번호와 1:1.
- 검증 항목(M2): `AISLE_WIDTH_MIN`(통로 폭·block)·`EXIT_ACCESS`(비상구·block)·`FLOOR_LOAD`(홀별 하중·block)·`HEIGHT_MAX`(block)·`MEZZANINE_RATIO`(block)·`CLEARANCE_WALL`(warn).
### `POST …/layout/auto-generate` — AI 자동배치(ORGANIZER)
요청 `AutoLayoutRequest`:
```json
{ "targetBoothCount":510, "premiumRatio":0.15, "stageCount":1, "loungeCount":1, "mainEntranceCount":2, "optionCount":3 }
```
응답 `AutoLayoutOption[]`:
```json
[ { "optionId":"opt-A", "label":"배치안 A",
"summary": { "boothCount":510, "targetBoothCount":510, "salesAreaM2":4420.0, "minAisleWidthM":3.2, "violationBlock":0, "violationWarn":0 },
"s7RenderJobId":"job-…" } ]
```
- `s7RenderJobId`: S7 홀 전경(조감) 생성 잡 — 완료 시 WebSocket `/topic/render/{jobId}` 푸시로 SCR-04 카드 이미지 교체.
> 스켈레톤 현황: `getLayout`·`saveLayout`·`validate`·`autoGenerate`는 `BoothMapper`(§8, PostGIS) 대기로 **501**. `ComplianceRuleEngine`(규정 평가기)·룰셋 로딩은 완성.
---
## 4. M3 부스 설계 스튜디오 (SCR-06/09) — **P0**
베이스: `/api/events/{eventId}/booths/{boothId}/design`
### `GET …/design?version={n}` — 열람(멤버/홀매니저)
응답 `DesignPlanDto`:
```json
{ "designId":"d-1","boothId":"b-102","version":2,"status":"draft",
"spec": DesignSpec, "updatedAt":"2026-07-11T…" }
```
### `PUT …/design` — 편집(EXHIBITOR/CONTRACTOR)
요청 `DesignSaveRequest`: `{ "version":2, "spec": DesignSpec }`. 응답: `DesignPlanDto`.
### `POST …/design/precheck` — 규정 사전검증(EXHIBITOR/CONTRACTOR/HALL_MANAGER) ✅완성
요청 `DesignSpec`:
```json
{
"boothType": "independent", "industry": "로봇/제조", "budget": 30000000,
"zones": [ { "type":"demo","ratioPercent":40 }, { "type":"consult","ratioPercent":30 } ],
"wallHeightM": 4.2, "signageText": "한빛로보틱스",
"rigging": { "use": true, "heightM": 7.0 },
"mezzanineAreaRatio": 0.3,
"materials": [ { "part":"wall","finish":"matte_white","fireRetardant": true } ],
"lightingMode": "night", "usesDesignatedLightingOnly": true
}
```
응답: `ComplianceReport`(§3-validate와 동일 shape). M3 평가 항목: `HEIGHT_MAX`(5m·block)·`RIGGING_RANGE`(6.5~8.5m·warn·`requiresDocument:"STRUCTURAL_CALC_D7"`)·`MEZZANINE_RATIO`(block)·`FIRE_RETARDANT`(block)·`CLEARANCE_CEILING`(warn)·`LIGHTING_BRING_IN`(warn).
- `wallHeightM > 5``HEIGHT_MAX` 차단, `measured:"측정 5.4m"`.
- 리깅 사용 시 D-7 구조계산서 부속 플래그(`requiresDocument`)를 SCR-09 칩으로 표시.
> 스켈레톤 현황: `precheck` **완성**(스펙→룰 엔진 즉시 평가). `getDesign`·`saveDesign`은 `DesignMapper`(§8) 대기로 **501**.
---
## 5. M4 유틸리티 설계 (SCR-07/08) — **P0**
베이스: `/api/events/{eventId}/booths/{boothId}/utility`
### `POST …/utility/quote` — 자동 견적(멤버) ✅완성
요청 `UtilityQuoteRequest`:
```json
{
"devices": [ { "name":"로봇 시연장비","count":2,"powerKw":1.5 },
{ "name":"LED 스포트","count":6,"powerKw":0.3 } ],
"networkWiredLines": 1, "plumbingOutlets": 1, "compressedAirOutlets": 0
}
```
응답 `UtilityQuote`:
```json
{
"totalPowerKw": 4.8, "requestedKw": 5, "distributionBox50A": 1,
"lines": [
{ "label":"전기 220V 단상 5kW","qty":5,"unitPrice":55000,"amount":275000 },
{ "label":"분전반 50A 추가","qty":1,"unitPrice":100000,"amount":100000 },
{ "label":"인터넷 유선 회선","qty":1,"unitPrice":150000,"amount":150000 },
{ "label":"급배수 구","qty":1,"unitPrice":150000,"amount":150000 }
],
"total": 675000, "currency":"KRW", "rulesetVersion":"rates-v1.0",
"disclaimer":"공시가 기준이며 최종 금액은 킨텍스 확정 시 안내됩니다."
}
```
- 산식: `requestedKw = ceil(Σ count×powerKw)`, `distributionBox50A = ceil(requestedKw / 11)`. 단가는 요율 룰셋(`rates-v1.0`).
### `POST …/utility/wiring?hallId={h}` — 배선 산출(멤버)
요청 `WiringRequest`:
```json
{ "terminals": [ { "kind":"power","position":[3.0,1.5],"kw":5 },
{ "kind":"network","position":[1.0,2.0] } ] }
```
응답 `WiringResult`:
```json
{ "assumedTrench": true,
"paths": [ { "kind":"power","color":"red","coords":[[1,1],[3,1.5]],"lengthM":2.06,"label":"5kW" },
{ "kind":"network","color":"blue","coords":[[0,2],[1,2]],"lengthM":1.0,"label":"유선 1회선" } ] }
```
- `assumedTrench=true` → SCR-07 "가정 트렌치 좌표(실측 대기)" 배지(PLANNING R4). 색상 규약: power=red·network=blue·plumbing/air=green(S6 동일).
### `POST …/utility/order` — 신청 제출(EXHIBITOR/CONTRACTOR)
요청 `UtilityQuoteRequest`(견적과 동일). 응답 `UtilityOrderDto`:
```json
{ "orderId":"uo-…","status":"submitted","quote": UtilityQuote,
"locationDiagramUrl":"/files/…/location-diagram.pdf",
"supplyTiming":"장치 마지막 날 오후",
"deadlineNotice":"유틸리티 신청 마감 D-25 · 인터넷은 현장 추가신청 불가",
"relayNotice":"주최자 사무국/킨텍스에 제출 파일이 릴레이됩니다." }
```
- `locationDiagramUrl`: 자동 생성 위치표시도(수기 작도 대체 서식, M4-3). 마감·릴레이 고지 포함(PLANNING R3).
### `GET …/utility` — 신청 조회(멤버). 응답: `UtilityOrderDto`.
> 스켈레톤 현황: `quote` **완성**(요율 룰셋 산술). `computeWiring`(`WiringMapper` PostGIS)·`submitOrder`(위치표시도 PDF·영속)·`getOrder`는 **501**.
---
## 6. M5 나노바나나 시각화 — RenderJob (SCR-06/12) — **P0 · G1 게이트**
> ★ Gemini 외부 호출은 소유자 승인(G1) 대상. 백엔드는 **큐 발행·상태·콜백까지만**(실호출은 Python 워커). 미승인 시에도 큐잉/상태는 동작.
### `POST /api/events/{eventId}/booths/{boothId}/render` — 발행(멤버) ✅완성(큐잉)
요청 `RenderJobRequest`:
```json
{ "shotPreset":"S1",
"scene": { "hall":{"id":"H7","dims_m":[126,90],"ceiling_m":12},
"booth":{"id":"A-102","size_m":[6,3],"type":"independent"},
"design":{"signage":{"text":"한빛로보틱스"}},
"lighting":{"mode":"day"} },
"referenceImageUrl":"/files/empty_booth.jpg" }
```
응답 `RenderJobDto`(QUEUED):
```json
{ "jobId":"<uuid>","boothId":"b-102","shotPreset":"S1","status":"QUEUED",
"imageUrl":null,"schemaHash":null,"modelVersion":null,
"watermarkRequired": true,
"watermarkText":"AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있습니다",
"notice":"AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 — 시공 기준은 도면입니다",
"errorMessage":null,"createdAt":"2026-07-11T…" }
```
- 쿼터 초과 시 `RENDER_QUOTA_EXCEEDED`(429). `scene`은 PLANNING §6-2 스키마 그대로 Redis 큐(`kintex:renderjob:queue`)에 적재 → 워커 소비.
- S6(배선 오버레이)는 워커의 백엔드 래스터 합성 경로(생성형 아님).
### `GET /api/events/{eventId}/render-jobs/{jobId}` — 상태(멤버) ✅완성
응답: `RenderJobDto`(status QUEUED|RUNNING|DONE|FAILED). DONE 시 `imageUrl` 채워짐. 없으면 `NOT_FOUND`.
### `GET /api/events/{eventId}/booths/{boothId}/render-jobs?shot={S1}` — 갤러리 목록(멤버)
응답: `RenderJobDto[]`(SCR-12). > 스켈레톤: `RenderJobMapper`(내구 이력) 대기로 **501**.
### `POST /api/internal/render/callback` — 워커 콜백(내부·공유 시크릿) ✅완성
헤더: `X-Worker-Token: <RENDER_WORKER_TOKEN>` (env). 요청 `WorkerCallbackRequest`:
```json
{ "jobId":"<uuid>","status":"DONE","imageUrl":"/files/…/S1.png",
"schemaHash":"…","modelVersion":"gemini-3.1-flash-image-preview","errorMessage":null }
```
- 처리: 상태 갱신 → WebSocket `/topic/render/{jobId}``RenderJobDto` 푸시. 성공 시에만 쿼터 차감(내구화는 매퍼).
- 실패(`status:"FAILED"`) 시 `errorMessage`는 요약만 통과(스택트레이스 유입 차단).
### WebSocket (STOMP)
- 핸드셰이크: `GET /ws`(SockJS). 브로드캐스트 prefix `/topic`, 클라→서버 `/app`.
- 구독: `/topic/render/{jobId}` → RenderJob 완료·실패 이벤트(design.md §1-4). (승인 이벤트 토픽은 M6/C-4에서 확장.)
---
## 7. 룰셋(버전 관리 데이터) 계약
- **규정** `rulesets/compliance-v1.json`(`compliance-v1.0`): 코드가 아닌 데이터. 개정 시 파일 교체. 리포트에 `rulesetVersion`·`disclaimer` 항상 기록(감사·면책, PLANNING R2).
- **요율** `rulesets/rates-v1.json`(`rates-v1.0`): 임대·유틸리티 단가. `quote` 응답의 `rulesetVersion` 근거.
- 연산자: `lte·gte·between·isTrue·eq·lteHall(홀별 상한)·excludesAll(금지목록)`. 측정값 없는 규칙은 미평가(리포트 카운트 제외).
---
## 8. DB 매퍼 인수 목록 (→ kintex-db-engineer)
> 아래 매퍼 인터페이스는 정의·주입 완료, **XML(PostGIS ST_*) 미구현 → 해당 API 501**. `resources/mybatis/mapper/` 에 구현.
| 매퍼 | 메서드 | 공간/쿼리 요지 |
|---|---|---|
| `auth.mapper.UserMapper` | `findAuthByEmail`, `findEventRoles` | 사용자 인증행(해시 응답 제외)·행사별 역할. `login`/`workspaces`/`accept-invite` |
| `module.m2.mapper.BoothMapper` | `findLayout`, `findBooths`, `upsertLayout`, `replaceBooths`, `sumSalesArea`, `minAisleWidth`, `countExitsBlocked` | 부스 POLYGON(ST_MakePolygon/ST_AsGeoJSON), 판매면적 ST_Area, 통로폭 ST_Distance/ST_Buffer, 비상구 ST_Intersects |
| `module.m3.mapper.DesignMapper` | `findDesign`, `upsertDesign`, `findEventIdByBooth` | DesignPlan 버전(spec jsonb), 부스→행사 역참조(RBAC) |
| `module.m4.mapper.WiringMapper` | `findNearestTrenches`, `shortestPath`, `isAssumedTrench` | 트렌치 POINT KNN(`<->`), 배선 LINESTRING 최단(ST_Length), 가정 그리드 플래그 |
| `module.m5.mapper.RenderJobMapper` | `insertJob`, `updateStatus`, `findByBooth`, `countSucceededByEvent` | RenderJob 내구 이력·쿼터 정본(Redis는 큐/실시간) |
### 필요 스키마(D-2 참조): `Event · User · EventMember · Hall · Trench · Booth(polygon) · Layout · DesignPlan · UtilityOrder(wiring LineString) · RenderJob · Company(등록업체)`. 홀 마스터·요율·규정 룰셋 시드는 D-2에서.
---
## 9. 변경 이력
| 버전 | 일자 | 내용 |
|---|---|---|
| v0.1 | 2026-07-11 | 최초 — 스캐폴드(S-1)와 함께 P0(M2·M3·M4·M5)+인증(C-1) 계약 정의. precheck·quote·render 큐잉·룰엔진 완성, 공간·영속 경로는 매퍼 대기(501). |