# 킨텍스 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`) ```json { "success": true, "data": { ... }, "error": null } { "success": false, "data": null, "error": { "code": "FORBIDDEN", "message": "이 행사/부스에 대한 권한이 없습니다." } } ``` - 목록은 `PageResponse` = `{ "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 ` (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": "", "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":"","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: ` (env). 요청 `WorkerCallbackRequest`: ```json { "jobId":"","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). |