diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteMapper.java index f2d3d92..9edabbb 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteMapper.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteMapper.java @@ -25,6 +25,7 @@ public interface PublicSiteMapper { to_char(e.end_date, 'YYYY-MM-DD') AS "endDate", e.status, h.label AS "hallLabel", + e.poster_url AS "posterUrl", (SELECT count(*)::int FROM booth b JOIN layout l ON l.id = b.layout_id @@ -63,6 +64,7 @@ public interface PublicSiteMapper { to_char(e.end_date, 'YYYY-MM-DD') AS "endDate", e.status, h.label AS "hallLabel", + e.poster_url AS "posterUrl", (SELECT count(*)::int FROM booth b JOIN layout l ON l.id = b.layout_id diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteService.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteService.java index e5b39a2..e3d6593 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteService.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicSiteService.java @@ -106,7 +106,8 @@ public class PublicSiteService { private PublicEventDto toEvent(Map m) { return new PublicEventDto( str(m.get("id")), str(m.get("name")), str(m.get("startDate")), str(m.get("endDate")), - str(m.get("status")), str(m.get("hallLabel")), toInt(m.get("boothCount"))); + str(m.get("status")), str(m.get("hallLabel")), toInt(m.get("boothCount")), + str(m.get("posterUrl"))); } private String generateReceiptNo() { diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicEventDto.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicEventDto.java index 5953a2f..7cd2646 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicEventDto.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicEventDto.java @@ -12,6 +12,7 @@ public record PublicEventDto( String endDate, String status, String hallLabel, - Integer boothCount + Integer boothCount, + String posterUrl ) { } diff --git a/src/backend/src/main/resources/db/migration/V46__poster_local_assets.sql b/src/backend/src/main/resources/db/migration/V46__poster_local_assets.sql new file mode 100644 index 0000000..da233ed --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V46__poster_local_assets.sql @@ -0,0 +1,111 @@ +-- V46__poster_local_assets.sql +-- 목적: event.poster_url 외부 핫링크(kintex.com imageView.do — 브라우저 안 뜸)를 +-- 빌드 내장 정적자산(/media/posters/kx--.jpg)으로 교체. +-- 원천: kintex.com 공개 홍보 포스터(내부 데모용). 크롤·축소(가로≤480, jpg q78, ~16~59KB). +-- 자산: src/frontend/public/media/posters/ → Vite dist 루트 → GET /media/posters/. +-- 멱등: URL 자연키 WHERE 조건 매칭. 재실행 시 이미 교체된 행은 매칭 안 됨(불변). +-- 정상/타 URL 및 미크롤 event 는 손대지 않음 → 그라디언트 폴백 유지. +-- 스코프: tenant_id='KINTEX'. V32·V39·V42 가 같은 URL 공유 시 일괄 갱신됨. + +UPDATE event SET poster_url = '/media/posters/kx-468958-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=468958&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-468981-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=468981&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469060-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469060&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469082-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469082&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469112-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469112&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469129-6.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469129&fileseq=6'; +UPDATE event SET poster_url = '/media/posters/kx-469133-36.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469133&fileseq=36'; +UPDATE event SET poster_url = '/media/posters/kx-469147-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469147&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469162-9.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469162&fileseq=9'; +UPDATE event SET poster_url = '/media/posters/kx-469163-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469163&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469174-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469174&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469177-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469177&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469185-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469185&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469186-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469186&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469201-3.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469201&fileseq=3'; +UPDATE event SET poster_url = '/media/posters/kx-469204-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469204&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469205-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469207-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469207&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469219-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469219&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469220-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469220&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469230-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469230&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469236-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469236&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469240-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469240&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469244-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469244&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469246-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469246&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469247-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469247&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469249-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469249&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469254-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469256-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469256&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469262-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469262&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469266-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469266&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469276-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469276&fileseq=1'; +UPDATE event SET poster_url = '/media/posters/kx-469277-2.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469277&fileseq=2'; +UPDATE event SET poster_url = '/media/posters/kx-469283-1.jpg' + WHERE tenant_id = 'KINTEX' + AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'; diff --git a/src/backend/src/main/resources/db/migration/V47__demo_live_event_closeloop.sql b/src/backend/src/main/resources/db/migration/V47__demo_live_event_closeloop.sql new file mode 100644 index 0000000..e11b731 --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V47__demo_live_event_closeloop.sql @@ -0,0 +1,215 @@ +-- ============================================================================ +-- V47 — 데모 로그인 해소 행사(e-2026-live) 폐루프 보강 (정산·옥션·관람객/리드·마케팅) +-- ============================================================================ +-- 배경(근본 원인): 데모 계정(demo-organizer/exhibitor/contractor)은 +-- ① e-2026-smf (V38, 8월 개막) ② e-2026-live (V39, 오늘 개막) 두 행사의 event_member. +-- 워크스페이스 정렬 = findEventRoles ORDER BY e.start_date → 오늘 개막인 e-2026-live 가 선두. +-- 프론트 useResolvedEventId = currentEventId ?? workspaces[0].eventId → 'e-2026-live'. +-- 문제: 정산(invoice/payment/tax/refund)·옥션(auction/bid/award)·관람객(visitor)·리드(lead)· +-- 마케팅(edm/sponsorship) 데모 데이터는 전부 e-2026-smf 또는 V42 bulk(bd-e-*)에만 존재 → +-- 로그인 후 해소되는 e-2026-live 스코프에서 정산·옥션·관람객 화면이 "빈 상태"로 보였다. +-- 조치: e-2026-live 를 축으로 각 도메인 폐루프를 멱등 보강(기존 시드 불변, ON CONFLICT DO NOTHING). +-- e-2026-live 참가업체(부스 assigned_company_name/booth_sale.sold_to_company): +-- (주)스마트오토메이션·델타로보틱스·글로벌테크·미래제조연구소·한국센서테크·바이오헬스랩 +-- PII 는 전부 더미(홍길동·010-0000-*·*@kintex.test). tenant_id='KINTEX'. +-- ============================================================================ + + +-- ============================================================================ +-- 1) 정산 폐루프 — 청구서(상태 분포) + 수납 + 세금계산서 + 환불 +-- ============================================================================ +-- invoice UNIQUE(event_id, exhibitor_name) → 행사당 참가업체명 1건(멱등). 상태 분포: +-- paid×2 · invoiced(부분수납)×2 · overdue×1 · pending×2 · cancelled(환불대상)×1 +INSERT INTO invoice (id, event_id, exhibitor_id, exhibitor_name, invoice_no, category, source_ref, + subtotal, vat, total, paid_amount, status, due_date, issued_at, paid_at, + cancelled_at, cancel_reason, line_items, note, created_by) VALUES + -- 완납 + ('inv-live-01','e-2026-live',NULL,'(주)스마트오토메이션','LIVE-2026-0001','rental',NULL, + 6534000, 653400, 7187400, 7187400, 'paid', CURRENT_DATE - 2, now()-interval '18 day', now()-interval '5 day', + NULL, NULL, + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ 아일랜드 · L-101)","quantity":18,"unitPrice":363000,"amount":6534000}]'::jsonb, + '완납 처리.', 'system-seed'), + ('inv-live-03','e-2026-live',NULL,'글로벌테크','LIVE-2026-0003','rental',NULL, + 4356000, 435600, 4791600, 4791600, 'paid', CURRENT_DATE - 1, now()-interval '10 day', now()-interval '3 day', + NULL, NULL, + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ 조립부스 · L-103)","quantity":18,"unitPrice":242000,"amount":4356000}]'::jsonb, + '완납 처리.', 'system-seed'), + -- 부분수납(발행) + ('inv-live-02','e-2026-live',NULL,'델타로보틱스','LIVE-2026-0002','rental',NULL, + 5445000, 544500, 5989500, 2395800, 'invoiced', CURRENT_DATE + 7, now()-interval '14 day', NULL, + NULL, NULL, + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ 독립부스 · L-102)","quantity":18,"unitPrice":302500,"amount":5445000}]'::jsonb, + '계약금 40% 수납.', 'system-seed'), + ('inv-live-06','e-2026-live',NULL,'바이오헬스랩','LIVE-2026-0006','rental',NULL, + 4356000, 435600, 4791600, 1916640, 'invoiced', CURRENT_DATE + 10, now()-interval '6 day', NULL, + NULL, NULL, + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ 조립부스 · L-108)","quantity":18,"unitPrice":242000,"amount":4356000}]'::jsonb, + '계약금 40% 수납.', 'system-seed'), + -- 연체(overdue) — 대시보드 overdueCount 활성화 + ('inv-live-04','e-2026-live',NULL,'미래제조연구소','LIVE-2026-0004','rental',NULL, + 4791600, 479160, 5270760, 0, 'overdue', CURRENT_DATE - 5, now()-interval '20 day', NULL, + NULL, NULL, + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ 코너부스 · L-104)","quantity":18,"unitPrice":266200,"amount":4791600}]'::jsonb, + '납부 기한 경과 — 독촉 대상.', 'system-seed'), + -- 발행 대기(pending) + ('inv-live-05','e-2026-live',NULL,'한국센서테크','LIVE-2026-0005','rental',NULL, + 4356000, 435600, 4791600, 0, 'pending', CURRENT_DATE + 14, NULL, NULL, + NULL, NULL, + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ 조립부스 · L-106)","quantity":18,"unitPrice":242000,"amount":4356000}]'::jsonb, + '발행 대기.', 'system-seed'), + -- 옥션 낙찰 수수료(F084) — AUC-LIVE-01 낙찰(aw-live-01, co-0001) 5% 수수료 + ('inv-live-07','e-2026-live',NULL,'(주)한빛부스디자인','LIVE-2026-0007','auction_fee','aw-live-01', + 210000, 21000, 231000, 0, 'pending', CURRENT_DATE + 12, NULL, NULL, + NULL, NULL, + '[{"category":"AUCTION_FEE","description":"옥션 낙찰 수수료 5% (AUC-LIVE-01 · 낙찰 4,200,000원)","quantity":1,"unitPrice":210000,"amount":210000}]'::jsonb, + '옥션 낙찰 수수료 청구(발행 대기).', 'system-seed'), + -- 취소(환불 대상) — 계약금 납입 후 취소 + ('inv-live-08','e-2026-live',NULL,'넥스트이노베이션','LIVE-2026-0008','rental',NULL, + 6000000, 600000, 6600000, 2640000, 'cancelled', CURRENT_DATE + 9, now()-interval '16 day', NULL, + now()-interval '2 day', '참가 철회(내부 사정) — 환불 규정 적용', + '[{"category":"RENTAL","description":"부스 임대료 (18㎡ · 추가배정)","quantity":18,"unitPrice":333333,"amount":6000000}]'::jsonb, + '취소 확정 — 환불 산정 대상.', 'system-seed') +ON CONFLICT (id) DO NOTHING; + +-- 수납 기록(분납·완납·취소 전 계약금) -------------------------------------------- +INSERT INTO invoice_payment (id, invoice_id, amount, method, memo, recorded_by, recorded_at) VALUES + ('pay-live-01-1','inv-live-01', 2874960, 'transfer', '계약금 40%', 'system-seed', now()-interval '18 day'), + ('pay-live-01-2','inv-live-01', 4312440, 'transfer', '잔금 60%', 'system-seed', now()-interval '5 day'), + ('pay-live-03-1','inv-live-03', 4791600, 'transfer', '일시 완납', 'system-seed', now()-interval '3 day'), + ('pay-live-02-1','inv-live-02', 2395800, 'transfer', '계약금 40%', 'system-seed', now()-interval '11 day'), + ('pay-live-06-1','inv-live-06', 1916640, 'transfer', '계약금 40%', 'system-seed', now()-interval '5 day'), + ('pay-live-08-1','inv-live-08', 2640000, 'transfer', '계약금 40%(취소 전 납입)', 'system-seed', now()-interval '13 day') +ON CONFLICT (id) DO NOTHING; + +-- 세금계산서(F083) 2건 — 완납 청구(inv-live-01·inv-live-03) 기반 ---------------------- +INSERT INTO tax_invoice (tenant_id, id, invoice_id, event_id, tax_invoice_no, + supplier_name, supplier_biz_no, supplier_ceo, supplier_addr, + buyer_name, supply_amount, vat, total, item_summary, status, issued_by) VALUES + ('KINTEX','tax-live-0001','inv-live-01','e-2026-live','TAX-LIVE-0001', + '킨텍스(KINTEX)','128-81-00000','대표이사','경기도 고양시 일산서구 킨텍스로 217', + '(주)스마트오토메이션', 6534000, 653400, 7187400, '부스 임대료 (18㎡ · L-101)', 'issued', 'system-seed'), + ('KINTEX','tax-live-0003','inv-live-03','e-2026-live','TAX-LIVE-0003', + '킨텍스(KINTEX)','128-81-00000','대표이사','경기도 고양시 일산서구 킨텍스로 217', + '글로벌테크', 4356000, 435600, 4791600, '부스 임대료 (18㎡ · L-103)', 'issued', 'system-seed') +ON CONFLICT (id) DO NOTHING; + +-- 환불(F085) 1건 — 취소 청구 inv-live-08 기준. base=실납입(2,640,000), 개막 D-일 구간 환불율 ----- +INSERT INTO invoice_refund (tenant_id, id, invoice_id, event_id, base_amount, refund_rate_percent, + refund_amount, forfeit_amount, days_before_event, bracket_label, + reason, ruleset_version, status, created_by) VALUES + ('KINTEX','ref-live-0008','inv-live-08','e-2026-live', 2640000, 50.00, + 1320000, 1320000, 0, '개막일 이후 취소(50%)', + '참가 철회 — 환불 규정(refund-v1.0) 구간 적용', 'refund-v1.0', 'recorded', 'system-seed') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 2) 옥션 폐루프 — 옥션 2(낙찰 1·진행 1) + 초대 + 응찰 + 낙찰 +-- ============================================================================ +INSERT INTO auction (id, event_id, title, category, auction_type, award_criteria, + weight_price, weight_reputation, weight_delivery, round, deadline, + material_package_id, materials, accent, created_by) VALUES + ('AUC-LIVE-01', 'e-2026-live', 'L-101 아일랜드 부스 장치공사', '전시디자인설치', 'reverse', 'comprehensive', + 60, 25, 15, 1, now() - interval '2 day', NULL, 'layout,design,boq', 'primary', 'demo-organizer'), + ('AUC-LIVE-02', 'e-2026-live', '메인 통로 전기·조명 설치', '전기/조명', 'reverse', 'comprehensive', + 55, 25, 20, 1, now() + interval '3 day', NULL, 'layout,boq', 'success', 'demo-organizer') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO auction_invite (id, auction_id, company_id) VALUES + ('ai-live-01-1', 'AUC-LIVE-01', 'co-0001'), + ('ai-live-01-2', 'AUC-LIVE-01', 'co-0002'), + ('ai-live-01-3', 'AUC-LIVE-01', 'co-0005'), + ('ai-live-02-1', 'AUC-LIVE-02', 'co-0003'), + ('ai-live-02-2', 'AUC-LIVE-02', 'co-0006'), + ('ai-live-02-3', 'AUC-LIVE-02', 'co-0001') +ON CONFLICT (auction_id, company_id) DO NOTHING; + +-- 응찰(견적) — (auction,company) 당 1행. AUC-LIVE-01 은 마감(낙찰), AUC-LIVE-02 는 진행 중. +INSERT INTO bid (id, auction_id, company_id, round, subtotal, vat, total, lead_days) VALUES + ('bid-live-01-1', 'AUC-LIVE-01', 'co-0001', 1, 4200000, 420000, 4200000, 8), + ('bid-live-01-2', 'AUC-LIVE-01', 'co-0002', 1, 4550000, 455000, 4550000, 7), + ('bid-live-01-3', 'AUC-LIVE-01', 'co-0005', 1, 4380000, 438000, 4380000, 10), + ('bid-live-02-1', 'AUC-LIVE-02', 'co-0003', 1, 3800000, 380000, 3800000, 6), + ('bid-live-02-2', 'AUC-LIVE-02', 'co-0006', 1, 3650000, 365000, 3650000, 9) +ON CONFLICT (auction_id, company_id) DO NOTHING; + +-- 낙찰(award) — AUC-LIVE-01 → co-0001 최저·종합 우수(수수료 청구 inv-live-07 source_ref 연동) +INSERT INTO award (id, auction_id, bid_id, reason, awarded_by, awarded_at) VALUES + ('aw-live-01', 'AUC-LIVE-01', 'bid-live-01-1', + '종합평가 1위(가격 60·평판 25·납기 15) — 최저 응찰가 및 납기 준수 이력 우수.', 'demo-organizer', now()-interval '1 day') +ON CONFLICT (auction_id) DO NOTHING; + + +-- ============================================================================ +-- 3) 관람객·리드 폐루프 — 사전등록(체크인 분포) + 부스 리드(핫리드 포함) +-- ============================================================================ +-- checkin_at 은 CURRENT_DATE 기준 상대(now()) — 재배포 시점의 '오늘' 체크인으로 집계(홈 KPI). +INSERT INTO visitor_registration + (id, event_id, name, phone, email, company, visitor_type, agree_privacy, agree_marketing, + badge_code, badge_issued, checkin_state, qr_token, checkin_at, registered_at) VALUES + ('vr-live-101','e-2026-live','홍길동','010-0000-3101','livevis101@kintex.test','(주)스마트오토메이션','buyer', true,true, 'KTX-L-0101',true, 'done', 'QR-LIVE0000000000000101', now()-interval '3 hour', now()-interval '2 day'), + ('vr-live-102','e-2026-live','김철수','010-0000-3102','livevis102@kintex.test','델타로보틱스', 'visitor',true,true, 'KTX-L-0102',true, 'done', 'QR-LIVE0000000000000102', now()-interval '2 hour', now()-interval '2 day'), + ('vr-live-103','e-2026-live','이영희','010-0000-3103','livevis103@kintex.test','한국센서테크', 'vip', true,false,'KTX-L-0103',true, 'done', 'QR-LIVE0000000000000103', now()-interval '90 minute', now()-interval '1 day'), + ('vr-live-104','e-2026-live','박민수','010-0000-3104','livevis104@kintex.test',NULL, 'visitor',true,true, 'KTX-L-0104',true, 'done', 'QR-LIVE0000000000000104', now()-interval '40 minute', now()-interval '1 day'), + ('vr-live-105','e-2026-live','최지우','010-0000-3105','livevis105@kintex.test','글로벌테크', 'buyer', true,true, 'KTX-L-0105',true, 'waiting','QR-LIVE0000000000000105', NULL, now()-interval '20 hour'), + ('vr-live-106','e-2026-live','정하늘','010-0000-3106','livevis106@kintex.test','미래제조연구소', 'visitor',true,false,'KTX-L-0106',false,'waiting','QR-LIVE0000000000000106', NULL, now()-interval '6 hour'), + ('vr-live-107','e-2026-live','강보람','010-0000-3107','livevis107@kintex.test','바이오헬스랩', 'buyer', true,true, 'KTX-L-0107',true, 'done', 'QR-LIVE0000000000000107', now()-interval '25 minute', now()-interval '5 hour') +ON CONFLICT (id) DO NOTHING; + +-- 리드(SCR-32) — 부스 스코프 + demo-exhibitor 수집. 핫리드(score>=80) 2건 포함. +INSERT INTO lead + (id, event_id, booth_id, exhibitor_id, name, phone, email, role, company, product, + interest, score, ai_reasons, activity, followup_draft, ai_generated, collected_at) VALUES + ('ld-live-001','e-2026-live','booth-live-02','demo-exhibitor','홍길동','010-0000-4001','lead001@kintex.test','생산기술 팀장','(주)스마트오토메이션','스마트 물류 AMR',5,92, + '["부스 체류 20분(평균 대비 320%)","사양서 다운로드 3회","구매 결정권자 직급"]'::jsonb, + '[{"text":"AMR 시연 참관","at":"오늘 13:40"},{"text":"견적 요청 폼 제출","at":"오늘 13:55"}]'::jsonb, + '안녕하세요, 홍 팀장님. 스마트 물류 AMR 커스텀 제안서를 준비했습니다. 다음 주 미팅 가능하실까요?', true, now()-interval '4 hour'), + ('ld-live-002','e-2026-live','booth-live-01','demo-exhibitor','김철수','010-0000-4002','lead002@kintex.test','구매 담당','델타로보틱스','AI 비전 검사 모듈',5,85, + '["AI 비전 데모 2회 재방문","경쟁사 대비 견적 문의","도입 시기 명확(Q4)"]'::jsonb, + '[{"text":"AI 비전 모듈 상담","at":"오늘 11:20"}]'::jsonb, + '안녕하세요, 김 님. 요청하신 AI 비전 검사 모듈 견적서와 파일럿 프로그램 안내드립니다.', true, now()-interval '6 hour'), + ('ld-live-003','e-2026-live','booth-live-03','demo-exhibitor','이영희','010-0000-4003','lead003@kintex.test','기술 기획','글로벌테크','협동로봇 HR-A1',3,64, + '["협동로봇 세션 참석","브로슈어 다운로드"]'::jsonb, + '[{"text":"데모 영상 시청","at":"오늘 10:10"}]'::jsonb, + '안녕하세요, 이 님. 협동로봇 HR-A1 도입 사례집을 보내드립니다.', true, now()-interval '8 hour'), + ('ld-live-004','e-2026-live','booth-live-06','demo-exhibitor','박민수','010-0000-4004','lead004@kintex.test','인프라 엔지니어','한국센서테크','클라우드 모니터링',2,43, + '["짧은 부스 체류","자료 열람 없음"]'::jsonb, + '[{"text":"부스 QR 스캔","at":"오늘 09:50"}]'::jsonb, + '안녕하세요, 박 님. 클라우드 모니터링 소개 자료를 보내드립니다.', true, now()-interval '9 hour'), + ('ld-live-005','e-2026-live','booth-live-02','demo-exhibitor','최지우','010-0000-4005','lead005@kintex.test','설비 담당','미래제조연구소','스마트팩토리 MES',4,71, + '["MES 상담 15분","도입 예산 문의"]'::jsonb, + '[{"text":"MES 상담","at":"오늘 12:05"}]'::jsonb, + '안녕하세요, 최 님. 스마트팩토리 MES 데모 환경 안내드립니다.', true, now()-interval '5 hour'), + ('ld-live-006','e-2026-live','booth-live-01','demo-exhibitor','정하늘','010-0000-4006','lead006@kintex.test','R&D','바이오헬스랩','바이오 계측 센서',3,58, + '["센서 데모 관람","카탈로그 열람"]'::jsonb, + '[{"text":"센서 데모","at":"오늘 14:20"}]'::jsonb, + '안녕하세요, 정 님. 바이오 계측 센서 카탈로그와 견적 가이드를 보내드립니다.', true, now()-interval '2 hour') +ON CONFLICT (id) DO NOTHING; + + +-- ============================================================================ +-- 4) 마케팅 폐루프 — EDM 캠페인 + 스폰서십 패키지/스폰서 +-- ============================================================================ +INSERT INTO edm_campaign (id, event_id, name, status, audience, meta, open_rate, click_rate) VALUES + ('cp-live-001','e-2026-live','스마트 물류·자동화 코리아 개막 안내','done', 5400, '발송 완료: 1일 전', '48.2%', '12.6%'), + ('cp-live-002','e-2026-live','VIP 바이어 상담회 초대','scheduled', 1200, '발송 예정: 오늘 18:00', '-', '-'), + ('cp-live-003','e-2026-live','현장 등록 관람객 만족도 설문','draft', 3100, '최종 수정: 2시간 전', '-', '-') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO sponsorship_package (id, event_id, code, name, name_ko, price, accent, benefits, total_qty, sold_qty, dday, sort_order) VALUES + ('sp-live-dia','e-2026-live','diamond','DIAMOND','다이아몬드', 30000000, 'primary', + '["메인 배너 노출","기조연설 세션","VIP 라운지 부스","공식 도록 전면 광고"]'::jsonb, 2, 1, 3, 0), + ('sp-live-gld','e-2026-live','gold','GOLD','골드', 15000000, 'warning', + '["로비 배너","세션 슬롯 1","도록 반면 광고"]'::jsonb, 4, 2, 3, 1), + ('sp-live-slv','e-2026-live','silver','SILVER','실버', 7000000, 'muted', + '["디지털 사이니지 노출","도록 로고 게재"]'::jsonb, 6, 3, 3, 2) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO sponsorship_sponsor (id, event_id, package_id, name, tier_label, contract_status, fulfillment) VALUES + ('ss-live-001','e-2026-live','sp-live-dia','(주)스마트오토메이션','DIAMOND','signed', + '[{"item":"메인 배너","done":true},{"item":"기조연설","done":false}]'::jsonb), + ('ss-live-002','e-2026-live','sp-live-gld','델타로보틱스','GOLD','signed', + '[{"item":"로비 배너","done":true}]'::jsonb), + ('ss-live-003','e-2026-live','sp-live-slv','한국센서테크','SILVER','pending', + '[]'::jsonb) +ON CONFLICT (id) DO NOTHING; diff --git a/src/frontend/public/media/posters/kx-468958-1.jpg b/src/frontend/public/media/posters/kx-468958-1.jpg new file mode 100644 index 0000000..2d1f3fa Binary files /dev/null and b/src/frontend/public/media/posters/kx-468958-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-468981-1.jpg b/src/frontend/public/media/posters/kx-468981-1.jpg new file mode 100644 index 0000000..29fee3d Binary files /dev/null and b/src/frontend/public/media/posters/kx-468981-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469060-1.jpg b/src/frontend/public/media/posters/kx-469060-1.jpg new file mode 100644 index 0000000..d8402de Binary files /dev/null and b/src/frontend/public/media/posters/kx-469060-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469082-1.jpg b/src/frontend/public/media/posters/kx-469082-1.jpg new file mode 100644 index 0000000..c0c5ff5 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469082-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469112-1.jpg b/src/frontend/public/media/posters/kx-469112-1.jpg new file mode 100644 index 0000000..446f3da Binary files /dev/null and b/src/frontend/public/media/posters/kx-469112-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469129-6.jpg b/src/frontend/public/media/posters/kx-469129-6.jpg new file mode 100644 index 0000000..ccc3076 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469129-6.jpg differ diff --git a/src/frontend/public/media/posters/kx-469133-36.jpg b/src/frontend/public/media/posters/kx-469133-36.jpg new file mode 100644 index 0000000..6553cf0 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469133-36.jpg differ diff --git a/src/frontend/public/media/posters/kx-469147-1.jpg b/src/frontend/public/media/posters/kx-469147-1.jpg new file mode 100644 index 0000000..53408a3 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469147-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469162-9.jpg b/src/frontend/public/media/posters/kx-469162-9.jpg new file mode 100644 index 0000000..73a70b6 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469162-9.jpg differ diff --git a/src/frontend/public/media/posters/kx-469163-2.jpg b/src/frontend/public/media/posters/kx-469163-2.jpg new file mode 100644 index 0000000..455b036 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469163-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469174-1.jpg b/src/frontend/public/media/posters/kx-469174-1.jpg new file mode 100644 index 0000000..df310f1 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469174-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469177-2.jpg b/src/frontend/public/media/posters/kx-469177-2.jpg new file mode 100644 index 0000000..07b9d3c Binary files /dev/null and b/src/frontend/public/media/posters/kx-469177-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469185-1.jpg b/src/frontend/public/media/posters/kx-469185-1.jpg new file mode 100644 index 0000000..7897541 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469185-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469186-2.jpg b/src/frontend/public/media/posters/kx-469186-2.jpg new file mode 100644 index 0000000..161c475 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469186-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469201-3.jpg b/src/frontend/public/media/posters/kx-469201-3.jpg new file mode 100644 index 0000000..6ad7e3b Binary files /dev/null and b/src/frontend/public/media/posters/kx-469201-3.jpg differ diff --git a/src/frontend/public/media/posters/kx-469204-1.jpg b/src/frontend/public/media/posters/kx-469204-1.jpg new file mode 100644 index 0000000..34e17d0 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469204-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469205-1.jpg b/src/frontend/public/media/posters/kx-469205-1.jpg new file mode 100644 index 0000000..a373c4c Binary files /dev/null and b/src/frontend/public/media/posters/kx-469205-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469207-1.jpg b/src/frontend/public/media/posters/kx-469207-1.jpg new file mode 100644 index 0000000..6894f3e Binary files /dev/null and b/src/frontend/public/media/posters/kx-469207-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469219-1.jpg b/src/frontend/public/media/posters/kx-469219-1.jpg new file mode 100644 index 0000000..c668dd7 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469219-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469220-1.jpg b/src/frontend/public/media/posters/kx-469220-1.jpg new file mode 100644 index 0000000..71ac47f Binary files /dev/null and b/src/frontend/public/media/posters/kx-469220-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469230-2.jpg b/src/frontend/public/media/posters/kx-469230-2.jpg new file mode 100644 index 0000000..9ea59f4 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469230-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469236-1.jpg b/src/frontend/public/media/posters/kx-469236-1.jpg new file mode 100644 index 0000000..8a9c3d4 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469236-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469240-1.jpg b/src/frontend/public/media/posters/kx-469240-1.jpg new file mode 100644 index 0000000..8fa82a2 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469240-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469244-1.jpg b/src/frontend/public/media/posters/kx-469244-1.jpg new file mode 100644 index 0000000..f13ce47 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469244-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469246-2.jpg b/src/frontend/public/media/posters/kx-469246-2.jpg new file mode 100644 index 0000000..3f0334c Binary files /dev/null and b/src/frontend/public/media/posters/kx-469246-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469247-1.jpg b/src/frontend/public/media/posters/kx-469247-1.jpg new file mode 100644 index 0000000..6735935 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469247-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469249-2.jpg b/src/frontend/public/media/posters/kx-469249-2.jpg new file mode 100644 index 0000000..4b906a6 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469249-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469254-2.jpg b/src/frontend/public/media/posters/kx-469254-2.jpg new file mode 100644 index 0000000..403d265 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469254-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469256-2.jpg b/src/frontend/public/media/posters/kx-469256-2.jpg new file mode 100644 index 0000000..ec0ba94 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469256-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469262-1.jpg b/src/frontend/public/media/posters/kx-469262-1.jpg new file mode 100644 index 0000000..cdd7663 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469262-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469266-1.jpg b/src/frontend/public/media/posters/kx-469266-1.jpg new file mode 100644 index 0000000..34f405f Binary files /dev/null and b/src/frontend/public/media/posters/kx-469266-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469276-1.jpg b/src/frontend/public/media/posters/kx-469276-1.jpg new file mode 100644 index 0000000..0dbc48f Binary files /dev/null and b/src/frontend/public/media/posters/kx-469276-1.jpg differ diff --git a/src/frontend/public/media/posters/kx-469277-2.jpg b/src/frontend/public/media/posters/kx-469277-2.jpg new file mode 100644 index 0000000..604b657 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469277-2.jpg differ diff --git a/src/frontend/public/media/posters/kx-469283-1.jpg b/src/frontend/public/media/posters/kx-469283-1.jpg new file mode 100644 index 0000000..bf64023 Binary files /dev/null and b/src/frontend/public/media/posters/kx-469283-1.jpg differ diff --git a/src/frontend/src/i18n/locales/en.json b/src/frontend/src/i18n/locales/en.json index 9dcd52a..bf3044d 100644 --- a/src/frontend/src/i18n/locales/en.json +++ b/src/frontend/src/i18n/locales/en.json @@ -2366,6 +2366,24 @@ "eventsCount": "{{count}} events", "prevMonth": "Previous month", "nextMonth": "Next month" + }, + "stats": { + "aria": "KINTEX at a glance", + "exhibitions": { + "label": "Total exhibitions", + "countTo": "1200", + "suffix": "+" + }, + "exhibitors": { + "label": "Exhibitors", + "countTo": "30000", + "suffix": "+" + }, + "visitors": { + "label": "Annual visitors", + "countTo": "5", + "suffix": "M+" + } } }, "business": { diff --git a/src/frontend/src/i18n/locales/ja.json b/src/frontend/src/i18n/locales/ja.json index 17ed7f2..a040be4 100644 --- a/src/frontend/src/i18n/locales/ja.json +++ b/src/frontend/src/i18n/locales/ja.json @@ -2366,6 +2366,24 @@ "eventsCount": "{{count}}件", "prevMonth": "前の月", "nextMonth": "次の月" + }, + "stats": { + "aria": "KINTEXの規模", + "exhibitions": { + "label": "累計展示会", + "countTo": "1200", + "suffix": "+" + }, + "exhibitors": { + "label": "出展企業", + "countTo": "30000", + "suffix": "+" + }, + "visitors": { + "label": "年間来場者", + "countTo": "500", + "suffix": "万+" + } } }, "business": { diff --git a/src/frontend/src/i18n/locales/ko.json b/src/frontend/src/i18n/locales/ko.json index c0dd147..75419dd 100644 --- a/src/frontend/src/i18n/locales/ko.json +++ b/src/frontend/src/i18n/locales/ko.json @@ -2366,6 +2366,24 @@ "eventsCount": "{{count}}건", "prevMonth": "이전 달", "nextMonth": "다음 달" + }, + "stats": { + "aria": "킨텍스 전시 규모", + "exhibitions": { + "label": "누적 전시", + "countTo": "1200", + "suffix": "+" + }, + "exhibitors": { + "label": "참가업체", + "countTo": "30000", + "suffix": "+" + }, + "visitors": { + "label": "연 관람객", + "countTo": "500", + "suffix": "만+" + } } }, "business": { diff --git a/src/frontend/src/i18n/locales/zh.json b/src/frontend/src/i18n/locales/zh.json index be19c52..a2a22af 100644 --- a/src/frontend/src/i18n/locales/zh.json +++ b/src/frontend/src/i18n/locales/zh.json @@ -2366,6 +2366,24 @@ "eventsCount": "{{count}} 场", "prevMonth": "上个月", "nextMonth": "下个月" + }, + "stats": { + "aria": "KINTEX 规模", + "exhibitions": { + "label": "累计展会", + "countTo": "1200", + "suffix": "+" + }, + "exhibitors": { + "label": "参展企业", + "countTo": "30000", + "suffix": "+" + }, + "visitors": { + "label": "年度观众", + "countTo": "500", + "suffix": "万+" + } } }, "business": { diff --git a/src/frontend/src/screens/public/PublicGatewayPage.tsx b/src/frontend/src/screens/public/PublicGatewayPage.tsx index ed3b486..40afda0 100644 --- a/src/frontend/src/screens/public/PublicGatewayPage.tsx +++ b/src/frontend/src/screens/public/PublicGatewayPage.tsx @@ -32,17 +32,17 @@ export function PublicGatewayPage() { return ( - {/* 제품 히어로 */} -
+ {/* 제품 히어로 — 배경 실사진은 후속(별도). 현재 V-4 그라디언트+패턴 폴백(CLS 0). */} +
{t('track.gateway.aiWatermark')}
-
+
{t('track.gateway.eyebrow')} @@ -63,18 +63,19 @@ export function PublicGatewayPage() { {/* 3-트랙 대형 분기 카드 */}
-
+

{t('track.gateway.tracksTitle')}

{t('track.gateway.tracksSub')}

-
- {TRACK_CARDS.map(({ key, icon: Icon, to, primary }) => ( +
+ {TRACK_CARDS.map(({ key, icon: Icon, to, primary }, i) => (
@@ -94,7 +95,7 @@ export function PublicGatewayPage() { {/* 행사 하이라이트 레일 */}
-
+

{t('track.gateway.eventsTitle')}

{t('track.gateway.eventsSub')}

@@ -103,7 +104,7 @@ export function PublicGatewayPage() { {t('track.gateway.eventsMore')}
- +
diff --git a/src/frontend/src/screens/public/PublicShell.tsx b/src/frontend/src/screens/public/PublicShell.tsx index f46f212..f8a8eca 100644 --- a/src/frontend/src/screens/public/PublicShell.tsx +++ b/src/frontend/src/screens/public/PublicShell.tsx @@ -8,6 +8,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { IconMenu, IconClose, IconSearch } from './publicIcons'; import { LanguageSwitcher } from '../../components/ui/LanguageSwitcher'; import { useAuthStore } from '../../store/authStore'; +import { usePublicMotion } from './usePublicMotion'; import './public.css'; const GNB = [ @@ -126,9 +127,12 @@ export function PublicShell({ const { t } = useTranslation(); const [menuOpen, setMenuOpen] = useState(false); const ctaLabel = cta ?? t('shell.cta.register'); + // §3C-V 비주얼·모션 레이어 — 공개 트랙(.kxp) 하위에만 스코프 주입. + const rootRef = useRef(null); + usePublicMotion(rootRef); return ( -
+
diff --git a/src/frontend/src/screens/public/VisitorTrackPage.tsx b/src/frontend/src/screens/public/VisitorTrackPage.tsx index 9e7dbd8..ab12d60 100644 --- a/src/frontend/src/screens/public/VisitorTrackPage.tsx +++ b/src/frontend/src/screens/public/VisitorTrackPage.tsx @@ -71,16 +71,26 @@ const JOURNEY: { key: string; icon: Ico; to: string }[] = [ { key: 'checkin', icon: IconCheckCircle, to: '/login' }, ]; -function LinkOrAnchor({ to, className, children }: { to: string; className: string; children: React.ReactNode }) { +function LinkOrAnchor({ + to, + className, + children, + style, +}: { + to: string; + className: string; + children: React.ReactNode; + style?: React.CSSProperties; +}) { if (to.startsWith('#')) { return ( - + {children} ); } return ( - + {children} ); @@ -139,13 +149,14 @@ export function VisitorTrackPage() { return ( {/* ── 1. 풀블리드 히어로 + AI 관람 도우미(중심) ── STITCH-REPLACE:hero ── */} -
-
+ {/* 히어로 배경 실사진은 후속(별도) — 현재 V-4 그라디언트+패턴 폴백 렌더(CLS 0). */} +
+
{t('track.visitor.aiWatermark')} -
+

{t('track.visitor.hero.title')}

{t('track.visitor.hero.subtitle')}

@@ -162,18 +173,38 @@ export function VisitorTrackPage() {
+ {/* ── 1b. 통계 밴드(카운트업) ── */} +
+
+
+ {(['exhibitions', 'exhibitors', 'visitors'] as const).map((k, i) => ( +
+ + 0 + + {t(`track.visitor.stats.${k}.label`)} +
+ ))} +
+
+
+ {/* ── 2. AI 기능 쇼케이스 타일 ── STITCH-REPLACE:tiles ── */}
-
+

{t('track.visitor.aiTiles.title')}

{t('track.visitor.aiTiles.sub')}

-
- {AI_TILES.map(({ key, icon: Icon, to, soon }) => ( - +
+ {AI_TILES.map(({ key, icon: Icon, to, soon }, i) => ( + {t('ai.assistant.label')} {soon && {t('track.visitor.aiTiles.soon')}} @@ -198,7 +229,7 @@ export function VisitorTrackPage() { {/* ── 4. 진행 중·예정 전시 쇼케이스 ── */}
-
+

{t('track.visitor.showcaseTitle')}

{t('track.visitor.showcaseSub')}

@@ -211,7 +242,7 @@ export function VisitorTrackPage() { {/* ── 5. 관심사 맞춤 추천 레일(AI) ── */}
-
+

{t('track.visitor.recommendTitle')}{' '} @@ -242,7 +273,7 @@ export function VisitorTrackPage() { {/* ── 6. 방문 가이드 허브(2×3, 라이브 우선·정적 폴백) ── */}
-
+

{t('track.visitor.guideTitle')}

{t('track.visitor.guideSub')}

@@ -268,7 +299,7 @@ export function VisitorTrackPage() {
)} -
+
{useLiveGuides ? guides.slice(0, 6).map((g) => { const Icon = guideIcon(g.category, g.title); @@ -310,15 +341,15 @@ export function VisitorTrackPage() { {/* ── 7. 관람객 여정 3-스텝 ── */}
-
+

{t('track.visitor.journeyTitle')}

{t('track.visitor.journeySub')}

-
    +
      {JOURNEY.map(({ key, icon: Icon, to }, i) => ( -
    1. +
    2. {i + 1} @@ -346,7 +377,7 @@ export function VisitorTrackPage() { {/* ── 8. 공지·뉴스 ── */}
      -
      +

      {t('track.visitor.noticeTitle')}

      {t('track.visitor.noticeSub')}

      @@ -382,7 +413,7 @@ export function VisitorTrackPage() { {/* ── 9. 전시장 소개 밴드 ── */}
      -
      +
      diff --git a/src/frontend/src/screens/public/public.css b/src/frontend/src/screens/public/public.css index 870b087..b8fbfa0 100644 --- a/src/frontend/src/screens/public/public.css +++ b/src/frontend/src/screens/public/public.css @@ -7,6 +7,15 @@ .kxp { --kxp-maxw: 1200px; --kxp-hero-scrim: linear-gradient(90deg, rgba(16, 24, 40, 0.72) 0%, rgba(16, 24, 40, 0.15) 100%); + /* §3C-V V-2 모션 토큰(공개 트랙 루트 스코프 — 신규 색 토큰 없음) */ + --motion-fast: 150ms; + --motion-base: 240ms; + --motion-slow: 400ms; + --motion-hero: 600ms; + --motion-countup: 1200ms; + --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); + --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); + --ease-decelerate: cubic-bezier(0, 0, 0.2, 1); font-family: var(--font-sans); color: var(--color-neutral-700); background: var(--color-white); @@ -4001,3 +4010,224 @@ a.kxp-ai__cite:hover { .kxp-tabnum { font-variant-numeric: tabular-nums; } + +/* ═══════════════════════════════════════════════════════════════════════════ + §3C-V 공개 사이트 비주얼·모션 레이어 (V-7 프론트 계약) + transform/opacity·scrollLeft만 애니메이트 · prefers-reduced-motion 전면 존중 + ═══════════════════════════════════════════════════════════════════════════ */ + +/* ── ① 스크롤 리빌 ────────────────────────────────────────────────────────── */ +.pub-reveal { + opacity: 0; + transform: translateY(22px); + transition: + opacity var(--motion-slow) var(--ease-emphasized), + transform var(--motion-slow) var(--ease-emphasized); + will-change: opacity, transform; +} +.pub-reveal.is-visible { + opacity: 1; + transform: none; + will-change: auto; +} +/* 스태거 — 자식 순차 지연(--i 인덱스 * 70ms) */ +.pub-reveal--stagger > * { + opacity: 0; + transform: translateY(18px); + transition: + opacity var(--motion-slow) var(--ease-emphasized), + transform var(--motion-slow) var(--ease-emphasized); + transition-delay: calc(var(--i, 0) * 70ms); +} +.pub-reveal--stagger.is-visible > * { + opacity: 1; + transform: none; +} + +/* ── ② 통계 밴드 + 카운트업 ─────────────────────────────────────────────────── */ +.pub-statsband { + padding: 40px 0; + background: + radial-gradient(900px 400px at 85% -30%, rgba(109, 74, 255, 0.14) 0%, transparent 60%), + linear-gradient(135deg, var(--color-primary-700) 0%, var(--color-primary-600) 100%); + color: var(--color-on-accent); +} +.pub-statsband__grid { + display: grid; + grid-template-columns: 1fr; + gap: 20px; + text-align: center; +} +@media (min-width: 640px) { + .pub-statsband__grid { + grid-template-columns: repeat(3, 1fr); + } +} +.pub-stat { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 12px; +} +.pub-stat__num { + font-size: clamp(30px, 5vw, 46px); + font-weight: 800; + line-height: 1.05; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; + color: var(--color-on-accent); +} +.pub-stat__label { + font-size: var(--fs-body); + font-weight: 600; + color: rgba(255, 255, 255, 0.85); +} +@media (min-width: 640px) { + .pub-stat + .pub-stat { + border-left: 1px solid rgba(255, 255, 255, 0.18); + } +} + +/* ── ③ 시네마틱 히어로 (파랄랙스 레이어·인트로) ─────────────────────────────── */ +.pub-hero { + position: relative; +} +/* 파랄랙스 배경 레이어는 상하로 확장해 슬라이드 시 여백 노출 방지(CLS 0) */ +.kxp-vhero__bg[data-parallax], +.kxp-hero__bg[data-parallax] { + top: -9%; + bottom: -9%; + will-change: transform; +} +/* 전경(콘텐츠) 인트로 리빌 — 마운트 .is-in */ +.pub-hero__fg { + opacity: 0; + transform: translateY(16px); + transition: + opacity var(--motion-hero) var(--ease-emphasized), + transform var(--motion-hero) var(--ease-emphasized); +} +.pub-hero.is-in .pub-hero__fg { + opacity: 1; + transform: none; +} + +/* ── ④ 포스터 카드 리프트 + 이미지 줌 ──────────────────────────────────────── */ +.pub-card--lift { + position: relative; + transition: transform var(--motion-base) var(--ease-standard); +} +.pub-card--lift::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: var(--shadow-level2); + opacity: 0; + transition: opacity var(--motion-base) var(--ease-standard); + pointer-events: none; + z-index: -1; +} +.pub-card--lift:hover { + transform: translateY(-4px); +} +.pub-card--lift:hover::after, +.pub-card--lift:focus-within::after { + opacity: 1; +} +.pub-card__media { + overflow: hidden; +} +.pub-card__media img { + transition: transform var(--motion-base) var(--ease-standard); +} +.pub-card--lift:hover .pub-card__media img { + transform: scale(1.04); +} + +/* ── ⑤ 오토스크롤 레일(가로) ──────────────────────────────────────────────── */ +.kxp-eventgrid--rail { + display: flex; + grid-template-columns: none; + gap: 20px; + overflow-x: auto; + scroll-snap-type: x proximity; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + padding-bottom: 6px; +} +.kxp-eventgrid--rail > .kxp-ecard { + flex: 0 0 300px; + max-width: 300px; + scroll-snap-align: start; +} +@media (min-width: 1024px) { + .kxp-eventgrid--rail > .kxp-ecard { + flex-basis: 340px; + max-width: 340px; + } +} + +/* ── ⑥ 폴백 유틸(그라디언트 + 라인 패턴) ───────────────────────────────────── */ +.pub-fallback-gradient { + background: linear-gradient(135deg, var(--color-primary-700) 0%, var(--color-primary-600) 100%); +} +.pub-pattern-grid { + background-image: + linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px); + background-size: 28px 28px; +} +.pub-pattern-dots { + background-image: radial-gradient(rgba(255, 255, 255, 0.14) 1.4px, transparent 1.4px); + background-size: 22px 22px; +} +/* 포스터 폴백 라벨(이미지 부재/onerror 시) */ +.kxp-ecard__poster { + position: absolute; + inset: 0; +} +.kxp-ecard__posterfallback { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + text-align: center; + font-size: 14px; + font-weight: 700; + color: rgba(255, 255, 255, 0.95); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35); +} + +/* ── ⑦ prefers-reduced-motion — 장식 모션 전면 비활성 ──────────────────────── */ +@media (prefers-reduced-motion: reduce) { + .kxp .pub-reveal, + .kxp .pub-reveal--stagger > *, + .kxp .pub-hero__fg { + opacity: 1 !important; + transform: none !important; + transition: none !important; + } + .kxp .kxp-vhero__bg[data-parallax], + .kxp .kxp-hero__bg[data-parallax] { + transform: none !important; + top: 0; + bottom: 0; + will-change: auto; + } + .kxp .pub-card--lift, + .kxp .pub-card__media img { + transition: none; + } + .kxp .pub-card--lift:hover { + transform: none; + } + .kxp .pub-card--lift:hover .pub-card__media img { + transform: none; + } + .kxp .kxp-eventgrid--rail { + scroll-behavior: auto; + } +} diff --git a/src/frontend/src/screens/public/publicApi.ts b/src/frontend/src/screens/public/publicApi.ts index 5f34edb..c208263 100644 --- a/src/frontend/src/screens/public/publicApi.ts +++ b/src/frontend/src/screens/public/publicApi.ts @@ -14,6 +14,8 @@ export interface PublicEvent { status: string | null; hallLabel: string | null; boothCount: number | null; + /** V46: 빌드 내장 로컬 포스터 경로(/media/posters/*.jpg). 부재 시 그라디언트 폴백. */ + posterUrl?: string | null; } export interface PublicFloorplanHall { diff --git a/src/frontend/src/screens/public/trackShared.tsx b/src/frontend/src/screens/public/trackShared.tsx index cdf09af..35a27f0 100644 --- a/src/frontend/src/screens/public/trackShared.tsx +++ b/src/frontend/src/screens/public/trackShared.tsx @@ -57,8 +57,44 @@ export function usePublicEvents(max = 8): EventsState { return { events, loading, error }; } -/** 행사 카드 그리드 — SCR-P1 카드 자산 재사용. 빈/로딩/에러 인라인 처리. */ -export function EventGrid({ state }: { state: EventsState }) { +/** + * 행사 포스터 미디어 — 실 포스터(/media/posters/*.jpg) 우선, onerror·부재 시 브랜드 + * 그라디언트+라인 패턴 폴백(§3C-V V-4, CLS 0 · aspect-ratio 예약). + */ +function EventPoster({ name, posterUrl, eager }: { name: string; posterUrl?: string | null; eager?: boolean }) { + const [failed, setFailed] = useState(false); + const showImg = !!posterUrl && !failed; + return ( + <> + {showImg ? ( + setFailed(true)} + /> + ) : ( +
      + {name} +
      + )} + + ); +} + +/** + * 행사 카드 그리드 — SCR-P1 카드 자산 재사용. 빈/로딩/에러 인라인 처리. + * variant='rail' → 가로 오토스크롤 레일(§3C-V V-3, .pub-rail--auto). 기본 'grid'(불변). + */ +export function EventGrid({ + state, + variant = 'grid', +}: { + state: EventsState; + variant?: 'grid' | 'rail'; +}) { const { t } = useTranslation(); const { events, loading, error } = state; @@ -80,22 +116,18 @@ export function EventGrid({ state }: { state: EventsState }) { return

      {t('home.empty')}

      ; } + const isRail = variant === 'rail'; + const gridCls = isRail ? 'kxp-eventgrid kxp-eventgrid--rail pub-rail--auto' : 'kxp-eventgrid'; + return ( -
      +
      {events.map((ev) => { const status = deriveStatus(ev.startDate, ev.endDate); const d = dday(ev.startDate); return ( -
      -
      -
      +
      +
      + {status === 'ongoing' ? ( {t('home.ongoing')} ) : status === 'ended' ? ( diff --git a/src/frontend/src/screens/public/usePublicMotion.ts b/src/frontend/src/screens/public/usePublicMotion.ts new file mode 100644 index 0000000..155a414 --- /dev/null +++ b/src/frontend/src/screens/public/usePublicMotion.ts @@ -0,0 +1,196 @@ +/* + * 공개 트랙 비주얼·모션 오케스트레이터 (design.md §3C-V V-6·V-7). + * PublicShell 하위(.kxp)만 스코프. 선언형 클래스/속성을 스캔해 CSS-우선 모션을 배선: + * ① .pub-reveal → IntersectionObserver(threshold 0.15·once) → .is-visible + * ② .pub-countup → IO 진입 1회 카운트업(data-count-to·data-count-suffix) + * ③ [data-parallax] → rAF passive scroll 라이트 파랄랙스(데스크톱만) + * ④ .pub-rail--auto → 오토스크롤(hover/focus/visibility 정지, data-interval) + * ⑤ .pub-hero → 마운트 인트로 .is-in + * prefers-reduced-motion: 파랄랙스·오토스크롤·카운트업 애니 전면 비활성(최종 상태 즉시). + * transform/opacity·scrollLeft만 사용(레이아웃 프로퍼티 애니 금지). AppShell/모바일 미주입. + */ +import { useEffect, type RefObject } from 'react'; + +function fmt(n: number): string { + return Math.round(n).toLocaleString(); +} + +export function usePublicMotion(rootRef: RefObject): void { + useEffect(() => { + const root = rootRef.current; + if (!root || typeof window === 'undefined') return; + + const reduce = + typeof window.matchMedia === 'function' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const desktop = + typeof window.matchMedia === 'function' && + window.matchMedia('(min-width: 1024px)').matches; + + const cleanups: Array<() => void> = []; + const supportsIO = typeof IntersectionObserver !== 'undefined'; + + // ── ① 스크롤 리빌 ──────────────────────────────────────────────────────── + if (reduce || !supportsIO) { + root.querySelectorAll('.pub-reveal').forEach((n) => n.classList.add('is-visible')); + } else { + const revealIO = new IntersectionObserver( + (entries) => { + entries.forEach((e) => { + if (e.isIntersecting) { + e.target.classList.add('is-visible'); + revealIO.unobserve(e.target); + } + }); + }, + { threshold: 0.15 }, + ); + const observeReveals = () => + root + .querySelectorAll('.pub-reveal:not(.is-visible)') + .forEach((n) => revealIO.observe(n)); + observeReveals(); + cleanups.push(() => revealIO.disconnect()); + + // 비동기(행사·공지 등)로 뒤늦게 마운트되는 .pub-reveal 노드 포착. + let rafScan = 0; + const mo = new MutationObserver(() => { + if (rafScan) return; + rafScan = requestAnimationFrame(() => { + rafScan = 0; + observeReveals(); + }); + }); + mo.observe(root, { childList: true, subtree: true }); + cleanups.push(() => { + if (rafScan) cancelAnimationFrame(rafScan); + mo.disconnect(); + }); + } + + // ── ② 카운트업 ────────────────────────────────────────────────────────── + const counts = Array.from(root.querySelectorAll('.pub-countup')); + const finalize = (node: HTMLElement) => { + const to = parseFloat(node.dataset.countTo || '0'); + node.textContent = fmt(to) + (node.dataset.countSuffix || ''); + }; + if (reduce || !supportsIO) { + counts.forEach(finalize); + } else { + const runCount = (node: HTMLElement) => { + const to = parseFloat(node.dataset.countTo || '0'); + const suffix = node.dataset.countSuffix || ''; + const dur = 1200; + const start = performance.now(); + const tick = (now: number) => { + const p = Math.min(1, (now - start) / dur); + const eased = 1 - Math.pow(1 - p, 3); // ease-decelerate + node.textContent = fmt(to * eased) + suffix; + if (p < 1) requestAnimationFrame(tick); + }; + requestAnimationFrame(tick); + }; + const countIO = new IntersectionObserver( + (entries) => { + entries.forEach((e) => { + if (e.isIntersecting) { + runCount(e.target as HTMLElement); + countIO.unobserve(e.target); + } + }); + }, + { threshold: 0.4 }, + ); + counts.forEach((n) => countIO.observe(n)); + cleanups.push(() => countIO.disconnect()); + } + + // ── ③ 라이트 파랄랙스(데스크톱·모션 허용 시만) ────────────────────────────── + if (!reduce && desktop) { + const layers = Array.from(root.querySelectorAll('[data-parallax]')); + if (layers.length) { + let raf = 0; + const apply = () => { + raf = 0; + const vh = window.innerHeight || document.documentElement.clientHeight; + layers.forEach((l) => { + const rect = l.getBoundingClientRect(); + if (rect.bottom < -200 || rect.top > vh + 200) return; // 오프스크린 무시 + const factor = parseFloat(l.dataset.parallax || '0'); + const y = (rect.top + rect.height / 2 - vh / 2) * factor; + l.style.transform = `translate3d(0, ${y.toFixed(1)}px, 0)`; + }); + }; + const onScroll = () => { + if (raf) return; + raf = requestAnimationFrame(apply); + }; + window.addEventListener('scroll', onScroll, { passive: true }); + window.addEventListener('resize', onScroll, { passive: true }); + apply(); + cleanups.push(() => { + window.removeEventListener('scroll', onScroll); + window.removeEventListener('resize', onScroll); + if (raf) cancelAnimationFrame(raf); + layers.forEach((l) => { + l.style.transform = ''; + }); + }); + } + } + + // ── ④ 오토스크롤 레일 ────────────────────────────────────────────────────── + if (!reduce) { + root.querySelectorAll('.pub-rail--auto').forEach((rail) => { + const interval = Math.max(1500, parseInt(rail.dataset.interval || '4500', 10) || 4500); + let paused = false; + const step = () => { + if (paused) return; + const max = rail.scrollWidth - rail.clientWidth; + if (max <= 4) return; + const card = rail.querySelector('[data-rail-item]'); + const delta = card ? card.offsetWidth + 20 : Math.round(rail.clientWidth * 0.8); + let next = rail.scrollLeft + delta; + if (next >= max - 4) next = 0; + rail.scrollTo({ left: next, behavior: 'smooth' }); + }; + const timer = window.setInterval(step, interval); + const pause = () => { + paused = true; + }; + const resume = () => { + paused = false; + }; + const onVis = () => { + paused = document.hidden; + }; + rail.addEventListener('mouseenter', pause); + rail.addEventListener('mouseleave', resume); + rail.addEventListener('focusin', pause); + rail.addEventListener('focusout', resume); + document.addEventListener('visibilitychange', onVis); + cleanups.push(() => { + window.clearInterval(timer); + rail.removeEventListener('mouseenter', pause); + rail.removeEventListener('mouseleave', resume); + rail.removeEventListener('focusin', pause); + rail.removeEventListener('focusout', resume); + document.removeEventListener('visibilitychange', onVis); + }); + }); + } + + // ── ⑤ 히어로 인트로 ──────────────────────────────────────────────────────── + const heroes = Array.from(root.querySelectorAll('.pub-hero')); + if (reduce) { + heroes.forEach((h) => h.classList.add('is-in')); + } else { + const rafId = requestAnimationFrame(() => + requestAnimationFrame(() => heroes.forEach((h) => h.classList.add('is-in'))), + ); + cleanups.push(() => cancelAnimationFrame(rafId)); + } + + return () => cleanups.forEach((fn) => fn()); + }, [rootRef]); +} diff --git a/src/frontend/src/screens/schedule/KxCalendarView.tsx b/src/frontend/src/screens/schedule/KxCalendarView.tsx index 9302eb6..effb46e 100644 --- a/src/frontend/src/screens/schedule/KxCalendarView.tsx +++ b/src/frontend/src/screens/schedule/KxCalendarView.tsx @@ -290,7 +290,7 @@ export default function KxCalendarView({ key={it.id} type="button" className={`kx-cv-event${it.statusCd === 'DONE' ? ' is-done' : ''}`} - style={it.color ? { borderLeftColor: it.color } : undefined} + style={it.color ? ({ ['--kx-bar-color']: it.color } as CSSProperties) : undefined} title={it.title} onClick={(e) => { e.stopPropagation(); @@ -321,7 +321,7 @@ export default function KxCalendarView({ style={{ gridColumn: `${seg.startCol + 1} / span ${seg.span}`, gridRow: seg.lane + 1, - ...(it.color ? { borderLeftColor: it.color } : {}), + ...(it.color ? ({ ['--kx-bar-color']: it.color } as CSSProperties) : {}), }} title={it.title} onClick={(e) => { diff --git a/src/frontend/src/screens/schedule/calendarView.css b/src/frontend/src/screens/schedule/calendarView.css index 8d8b11b..ba9c7ab 100644 --- a/src/frontend/src/screens/schedule/calendarView.css +++ b/src/frontend/src/screens/schedule/calendarView.css @@ -4,6 +4,13 @@ .kx-cv-root { width: 100%; min-width: 0; + /* Nifty(app-views/calendar) 톤 — 캘린더 스코프 변수(전역 토큰만 사용). + 격자선은 옅게(neutral-100), today 는 옅은 노랑 틴트, 이벤트 칩 radius 3px. */ + --kx-cv-line: var(--color-neutral-100); /* 셀 격자선(프레임보다 옅게) */ + --kx-cv-cell-border: 1px solid var(--color-neutral-100); + --kx-cv-today-bg: var(--color-warning-bg); /* today 옅은 노랑 셀 틴트 */ + --kx-cv-bar-radius: 3px; + --kx-cv-bar-default: var(--color-primary-600); /* 카테고리 미매핑 폴백 = 소프트 브랜드 */ } /* ---- 툴바 ---- */ @@ -115,7 +122,7 @@ font-weight: 600; color: var(--color-neutral-500); background: var(--color-neutral-050); - border-bottom: var(--border-card); + border-bottom: 1px solid var(--color-neutral-200); } .kx-cv-dow.is-sun { color: var(--color-error); @@ -141,8 +148,8 @@ min-width: 0; /* 피드백②: 이벤트 없는 날 아래 대형 공백 → 셀 높이 축소(콘텐츠 밀도 정리). */ min-height: 64px; - border-top: var(--border-card); - border-right: var(--border-card); + border-top: var(--kx-cv-cell-border); + border-right: var(--kx-cv-cell-border); padding: var(--space-1) var(--space-2) var(--space-2); font-size: var(--fs-caption); background: var(--color-white); @@ -160,9 +167,17 @@ background: var(--color-neutral-050); color: var(--color-neutral-500); } +/* 다른 달 날짜 숫자 — 흐린 회색(muted). */ +.kx-cv-cell.is-out .kx-cv-daynum { + color: var(--color-neutral-500); + font-weight: 500; +} +/* 오늘 — 진한 아웃라인/알약 제거 → Nifty식 옅은 노랑 셀 틴트 + 숫자 강조. */ .kx-cv-cell.is-today { - outline: 2px solid var(--color-neutral-500); - outline-offset: -2px; + background: var(--kx-cv-today-bg); +} +.kx-cv-cell.is-today .kx-cv-daynum { + font-weight: 800; } .kx-cv-cell.is-selected { background: var(--color-primary-050); @@ -227,6 +242,8 @@ /* ---- 기간 막대(bar) ---- */ .kx-cv-bar { + /* 카테고리색은 컴포넌트가 --kx-bar-color 로 주입(미지정 시 소프트 브랜드 폴백). */ + --kx-bar-color: var(--kx-cv-bar-default); pointer-events: auto; min-width: 0; display: flex; @@ -235,10 +252,11 @@ margin: 0 1px; padding: 0 6px; border: none; - border-radius: var(--radius-sm); - background: var(--color-primary-050); - color: var(--color-primary-700); - border-left: 3px solid var(--color-primary-600); + border-radius: var(--kx-cv-bar-radius); + /* 소프트 틴트 배경 + 진한 카테고리 글자 + 얇은 좌측 액센트(Nifty 가벼운 칩). */ + background: color-mix(in srgb, var(--kx-bar-color) 14%, var(--color-white)); + color: color-mix(in srgb, var(--kx-bar-color) 72%, var(--color-neutral-900)); + border-left: 3px solid var(--kx-bar-color); font-size: 11px; line-height: var(--kx-cv-bar-h); overflow: hidden; @@ -247,8 +265,8 @@ text-align: left; } .kx-cv-bar:hover { - background: var(--color-primary-600); - color: #fff; + background: var(--kx-bar-color); + color: var(--color-on-accent); } .kx-cv-bar.is-cont-left { border-top-left-radius: 0; @@ -261,17 +279,27 @@ border-bottom-right-radius: 0; margin-right: 0; } +/* 완료(DONE) — 무거운 진회색 대신 아주 옅은 회색 muted 칩. */ .kx-cv-bar.is-done { + background: var(--color-neutral-050); color: var(--color-neutral-500); + border-left-color: var(--color-neutral-200); +} +.kx-cv-bar.is-done:hover { + background: var(--color-neutral-100); + color: var(--color-neutral-700); } .kx-cv-bar-badge { flex: 0 0 auto; margin-right: 4px; padding: 0 4px; border-radius: var(--radius-sm); - background: rgba(0, 0, 0, 0.08); + background: color-mix(in srgb, var(--kx-bar-color, var(--color-primary-600)) 22%, transparent); font-weight: 600; } +.kx-cv-bar:hover .kx-cv-bar-badge { + background: rgba(255, 255, 255, 0.25); +} .kx-cv-bar-title { overflow: hidden; text-overflow: ellipsis; @@ -328,8 +356,8 @@ .kx-cv-week-cell { min-width: 0; min-height: 200px; - border-top: var(--border-card); - border-right: var(--border-card); + border-top: var(--kx-cv-cell-border); + border-right: var(--kx-cv-cell-border); padding: var(--space-2); cursor: pointer; } @@ -337,8 +365,7 @@ border-right: none; } .kx-cv-week-cell.is-today { - outline: 2px solid var(--color-neutral-500); - outline-offset: -2px; + background: var(--kx-cv-today-bg); } .kx-cv-week-cell.is-selected { background: var(--color-primary-050); @@ -366,27 +393,36 @@ /* ---- 단일일 항목(칩)·뱃지 ---- */ .kx-cv-event { + --kx-bar-color: var(--kx-cv-bar-default); display: flex; align-items: center; max-width: 100%; margin-top: var(--space-1); padding: 2px 6px; border: none; - border-radius: var(--radius-sm); - background: var(--color-primary-050); - color: var(--color-primary-700); + border-radius: var(--kx-cv-bar-radius); + /* 소프트 카테고리 틴트 칩(Nifty). 미매핑 시 소프트 브랜드 폴백 — 진회색 금지. */ + background: color-mix(in srgb, var(--kx-bar-color) 14%, var(--color-white)); + color: color-mix(in srgb, var(--kx-bar-color) 72%, var(--color-neutral-900)); cursor: pointer; font-size: 11px; line-height: 1.4; overflow: hidden; white-space: nowrap; - border-left: 3px solid var(--color-primary-600); + border-left: 3px solid var(--kx-bar-color); text-align: left; width: 100%; } .kx-cv-event:hover { - background: var(--color-primary-600); - color: #fff; + background: var(--kx-bar-color); + color: var(--color-on-accent); +} +.kx-cv-event.is-done { + background: var(--color-neutral-050); + border-left-color: var(--color-neutral-200); +} +.kx-cv-event.is-done:hover { + background: var(--color-neutral-100); } .kx-cv-event.is-done .kx-cv-event-title { color: var(--color-neutral-500); diff --git a/src/frontend/src/screens/schedule/schedule.css b/src/frontend/src/screens/schedule/schedule.css index 5cf75d6..8af1d0d 100644 --- a/src/frontend/src/screens/schedule/schedule.css +++ b/src/frontend/src/screens/schedule/schedule.css @@ -208,13 +208,14 @@ color: var(--color-error); font-weight: 700; } +/* today — 진남색 알약 제거 → Nifty식 옅은 노랑 틴트 + 진한 숫자(회색 테두리 유지). */ .kx-cal__cell.is-today { - background: var(--color-primary-600); - color: var(--color-on-accent); + background: var(--color-warning-bg); + color: var(--color-primary-700); font-weight: 700; } .kx-cal__cell.is-today.has-event::after { - background: var(--color-white); + background: var(--color-primary-600); } /* 목록 건수 */ diff --git a/src/frontend/src/screens/settlement/settlement.css b/src/frontend/src/screens/settlement/settlement.css index b45dffb..fdf6437 100644 --- a/src/frontend/src/screens/settlement/settlement.css +++ b/src/frontend/src/screens/settlement/settlement.css @@ -6,6 +6,7 @@ .kx-settle { display: grid; gap: 16px; + padding: var(--gutter); } /* 헤더 */