Compare commits

..

No commits in common. "c3a695d70c1c025643e2522d4111aab70d89db4a" and "c7b853be17e21544d550d30e3322e27b5180ba07" have entirely different histories.

122 changed files with 575 additions and 25161 deletions

View File

@ -88,5 +88,3 @@
| 2026-07-11 | 모바일 트랙 하네스 신설(kintex-mobile-dev + kintex-mobile-orchestrator, WISE 모바일 레퍼런스·Stitch 디자인 경유·G3 EAS 게이트) + 웹 WISE 기능 레퍼런스 강화(kintex-frontend-dev·impl-orchestrator) | | 2026-07-11 | 모바일 트랙 하네스 신설(kintex-mobile-dev + kintex-mobile-orchestrator, WISE 모바일 레퍼런스·Stitch 디자인 경유·G3 EAS 게이트) + 웹 WISE 기능 레퍼런스 강화(kintex-frontend-dev·impl-orchestrator) |
| 2026-07-12 | **제안서.pptx 완성**(나라장터 표준 7대장·97슬라이드·RTM 142/142 QA 승인·화면갤러리 19시안) + 산출물 7종 전체 완성(개발계획서·지침서3·프로그램사양서·DA·사업수행계획서). PLANNING v3.1(계정통합·앱 2타깃)·design v2.1(84화면)·백로그 Phase F. Stitch 신규 24/64 확보(웹코어 트랙 진행·실패 12건은 서비스 저하 — 재시도 예약). 재생성: deliverables/제안서/_gen/build_deck.py | | 2026-07-12 | **제안서.pptx 완성**(나라장터 표준 7대장·97슬라이드·RTM 142/142 QA 승인·화면갤러리 19시안) + 산출물 7종 전체 완성(개발계획서·지침서3·프로그램사양서·DA·사업수행계획서). PLANNING v3.1(계정통합·앱 2타깃)·design v2.1(84화면)·백로그 Phase F. Stitch 신규 24/64 확보(웹코어 트랙 진행·실패 12건은 서비스 저하 — 재시도 예약). 재생성: deliverables/제안서/_gen/build_deck.py |
| 2026-07-12 | **Stitch 잔여 39화면 미확보 — 서비스 저하로 보류.** 확보 25/64(모바일 M4~M14·관리자 A1~A4/A7/A10·공개 P8·웹코어 18~21/24/28/31). 잔여: 웹코어 22/23/25~27/29/30/32~51(27)·관리자 A5/A6/A8/A9(4)·공개 P1~P7(7)·모바일 M15(1). generate 타임아웃 지속+list_screens 스테일(구 27건 고정). **회복 후 재실행 절차 = `_workspace/stitch_gen/harvest_report.md`**(수확 패스→재생성, 연속 3타임아웃 시 중단 규칙). Stitch 웹 UI(프로젝트 9385904003821333054) 육안 확인 권장. 부수: 로그인 슬라이드 CMS·RFP·보고서 4종 커밋(1de3b9b) | | 2026-07-12 | **Stitch 잔여 39화면 미확보 — 서비스 저하로 보류.** 확보 25/64(모바일 M4~M14·관리자 A1~A4/A7/A10·공개 P8·웹코어 18~21/24/28/31). 잔여: 웹코어 22/23/25~27/29/30/32~51(27)·관리자 A5/A6/A8/A9(4)·공개 P1~P7(7)·모바일 M15(1). generate 타임아웃 지속+list_screens 스테일(구 27건 고정). **회복 후 재실행 절차 = `_workspace/stitch_gen/harvest_report.md`**(수확 패스→재생성, 연속 3타임아웃 시 중단 규칙). Stitch 웹 UI(프로젝트 9385904003821333054) 육안 확인 권장. 부수: 로그인 슬라이드 CMS·RFP·보고서 4종 커밋(1de3b9b) |
| 2026-07-12 | **Stitch 전 화면 확보 완료(64/64)** — 서비스 회복 후 수확 패스 재실행: 잔여 39화면 전부 서버측 지연 생성 완료 상태(프로젝트 27→155 화면)로 재생성 0회·다운로드 43파일(보조 변형 5 포함: P4 3단계·P7 예매확인·M15 권종선택) 실패 0. `_workspace/stitch_gen/{harvest2.py,screen_titles.txt,harvest_report.md}`. 다음: frontend/mobile dev 이식 + design.md 🔲미생성 마커 갱신(designer 경유) |
| 2026-07-12 | **실데이터·2FA 트랙 마감** — ①실데이터 집계 API 5패키지(analytics·dashboard·ops·admin·catalog + V13 인덱스) ②TOTP 2FA 완결(SecretCipher AES-256-GCM·V12·OtpChallengePanel/OtpSetupPage·OTP_ENFORCE env) ③공통 업무기능 SCR-39~48 화면(`screens/work/`) 배선 ④M2 부스 겹침 검사(BOOTH_OVERLAP·compliance-v1.1) + 단위테스트 3종. 검증: backend compileJava+test·frontend tsc 통과, QA PASS(blocker/major 0, minor 2=RIGGING_RANGE semantics 소유자 결정 대기·운영 KINTEX_OTP_ENC_KEY 주입 게이트 — `_workspace/09_qa_track_close.md`). 계약 갭 기록: `_workspace/{07_work_api_gaps,08_m2m5_contract_changes}.md` |

View File

@ -1,35 +0,0 @@
package com.zioinfo.kintex.admin;
import com.zioinfo.kintex.admin.dto.AdminDashboardDto;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.ApiResponse;
import com.zioinfo.kintex.system.SystemAccessGuard;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 관리자 백오피스 대시보드 API (SCR-14). 시스템관리자(홀매니저/ADMIN) 접근.
*/
@RestController
@RequestMapping("/api/admin/dashboard")
public class AdminDashboardController {
private final AdminDashboardService service;
private final SystemAccessGuard guard;
public AdminDashboardController(AdminDashboardService service, SystemAccessGuard guard) {
this.service = service;
this.guard = guard;
}
/** GET — 전 행사 KPI·라이브 행사·(원천 있으면)관람객 추이. */
@GetMapping
public ApiResponse<AdminDashboardDto> get(@AuthenticationPrincipal KintexPrincipal principal,
@RequestParam(required = false, defaultValue = "KINTEX") String tenant) {
guard.requireAdmin(principal);
return ApiResponse.ok(service.getDashboard(tenant));
}
}

View File

@ -1,51 +0,0 @@
package com.zioinfo.kintex.admin;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 관리자 대시보드 집계 매퍼 event·hall_assignment·app_user·company read-only.
* 진행 여부는 오늘 날짜(start오늘end) 기준. 마이그레이션 불변.
*/
@Mapper
public interface AdminDashboardMapper {
/** KPI 원자 집계 — 진행 행사·사용 홀·활성 사용자·등록업체 수. */
@Select("""
SELECT
(SELECT count(*) FROM event
WHERE start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE) AS ongoingEvents,
(SELECT count(DISTINCT ha.hall_id) FROM hall_assignment ha JOIN event e ON e.id = ha.event_id
WHERE e.start_date <= CURRENT_DATE AND e.end_date >= CURRENT_DATE) AS hallsInUse,
(SELECT count(*) FROM app_user WHERE status = 'ACTIVE') AS activeUsers,
(SELECT count(*) FROM company) AS companies,
(SELECT count(*) FROM event
WHERE start_date > CURRENT_DATE) AS upcomingEvents
""")
Map<String, Object> findKpiCounts();
/** 진행 중(라이브) 행사 — 대표 홀·부스 수·홀 수용량. 최대 20건. */
@Select("""
SELECT e.id AS eventId,
e.name,
COALESCE(h.label, '') AS hall,
e.status,
(SELECT count(*) FROM booth b JOIN layout l ON l.id = b.layout_id
WHERE l.event_id = e.id) AS boothCount,
COALESCE((SELECT sum(h2.booth_capacity) FROM hall_assignment ha2
JOIN hall h2 ON h2.id = ha2.hall_id WHERE ha2.event_id = e.id), 0) AS capacity
FROM event e
LEFT JOIN LATERAL (
SELECT hall_id FROM hall_assignment ha
WHERE ha.event_id = e.id ORDER BY is_primary DESC LIMIT 1
) pa ON true
LEFT JOIN hall h ON h.id = pa.hall_id
WHERE e.start_date <= CURRENT_DATE AND e.end_date >= CURRENT_DATE
ORDER BY e.start_date DESC
LIMIT 20
""")
List<Map<String, Object>> findLiveEvents();
}

View File

@ -1,70 +0,0 @@
package com.zioinfo.kintex.admin;
import com.zioinfo.kintex.admin.dto.AdminDashboardDto;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 관리자 대시보드 서비스 테이블 집계로 KPI·라이브 행사를 조립.
* 관람객 추이는 센서/집계 인프라 부재 빈배열. 테넌트는 데이터(KINTEX) 반환.
*/
@Service
public class AdminDashboardService {
private final AdminDashboardMapper mapper;
public AdminDashboardService(AdminDashboardMapper mapper) {
this.mapper = mapper;
}
public AdminDashboardDto getDashboard(String tenant) {
Map<String, Object> c = mapper.findKpiCounts();
long ongoing = lng(c == null ? null : c.get("ongoingEvents"));
long halls = lng(c == null ? null : c.get("hallsInUse"));
long users = lng(c == null ? null : c.get("activeUsers"));
long companies = lng(c == null ? null : c.get("companies"));
long upcoming = lng(c == null ? null : c.get("upcomingEvents"));
List<AdminDashboardDto.Kpi> kpis = new ArrayList<>();
kpis.add(new AdminDashboardDto.Kpi("진행 행사", String.valueOf(ongoing), halls + "개 홀"));
kpis.add(new AdminDashboardDto.Kpi("예정 행사", String.valueOf(upcoming), null));
kpis.add(new AdminDashboardDto.Kpi("활성 사용자", String.valueOf(users), null));
kpis.add(new AdminDashboardDto.Kpi("등록업체", String.valueOf(companies), null));
List<AdminDashboardDto.LiveEvent> live = new ArrayList<>();
List<Map<String, Object>> rows = mapper.findLiveEvents();
if (rows != null) {
for (Map<String, Object> r : rows) {
long booths = lng(r.get("boothCount"));
long capacity = lng(r.get("capacity"));
int occupancy = capacity > 0 ? (int) Math.min(100, Math.round(booths * 100.0 / capacity)) : 0;
live.add(new AdminDashboardDto.LiveEvent(
str(r.get("eventId")), str(r.get("name")), str(r.get("hall")),
occupancy, "ongoing"));
}
}
// 관람객 추이: 센서/집계 원천 부재 빈배열(정직). 테넌트: 데이터 KINTEX .
List<AdminDashboardDto.VisitorPoint> visitorTrend = new ArrayList<>();
List<String> tenants = List.of("KINTEX");
return new AdminDashboardDto(kpis, visitorTrend, live, tenants);
}
private static long lng(Object o) {
if (o == null) return 0L;
if (o instanceof Number n) return n.longValue();
try {
return Long.parseLong(String.valueOf(o));
} catch (NumberFormatException e) {
return 0L;
}
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -1,25 +0,0 @@
package com.zioinfo.kintex.admin.dto;
import java.util.List;
/**
* 관리자 백오피스 대시보드 (SCR-14 / 06_backend_api_gaps §4).
* 집계원: event(진행/라이브)·hall_assignment·app_user·company. 관람객 추이는 센서/집계 부재 빈배열(정직).
*/
public record AdminDashboardDto(
List<Kpi> kpis,
List<VisitorPoint> visitorTrend,
List<LiveEvent> liveEvents,
List<String> tenants
) {
public record Kpi(String label, String value, String sub) {
}
/** 시간대별 관람객(원천 부재 시 미집계). */
public record VisitorPoint(String hour, long count) {
}
/** 라이브(진행 중) 행사 — 점유율=부스수/홀 수용. */
public record LiveEvent(String eventId, String name, String hall, int occupancy, String status) {
}
}

View File

@ -1,39 +0,0 @@
package com.zioinfo.kintex.analytics;
import com.zioinfo.kintex.analytics.dto.AnalyticsDto;
import com.zioinfo.kintex.auth.EventAccessGuard;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.ApiResponse;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 경영분석 BI API (SCR-13). 행사 멤버/홀매니저 열람.
* perspective(operator|exhibitor)·period(event|d90|annual) 집계 세트가 갈린다.
*/
@RestController
@RequestMapping("/api/events/{eventId}/analytics")
public class AnalyticsController {
private final AnalyticsService service;
private final EventAccessGuard guard;
public AnalyticsController(AnalyticsService service, EventAccessGuard guard) {
this.service = service;
this.guard = guard;
}
/** GET — KPI·매출 추이·손익·(원천 있으면)업종·참가업체 성과. */
@GetMapping
public ApiResponse<AnalyticsDto> get(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String eventId,
@RequestParam(required = false, defaultValue = "operator") String perspective,
@RequestParam(required = false, defaultValue = "event") String period) {
guard.requireEventAccess(principal, eventId);
return ApiResponse.ok(service.getAnalytics(eventId, perspective, period));
}
}

View File

@ -1,55 +0,0 @@
package com.zioinfo.kintex.analytics;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 경영분석 BI 집계 매퍼 booth(PostGIS ST_Area)·utility_order·master_data(요율) read-only.
* 매출은 판매면적×임대 요율(/·12h) 산출한다. 마이그레이션 불변.
*/
@Mapper
public interface AnalyticsMapper {
/** 전시홀 임대 요율(원/㎡) — master_data RATE.exhibitionHallPerM2. 없으면 null. */
@Select("""
SELECT (value_json #>> '{}')::numeric
FROM master_data WHERE category = 'RATE' AND code = 'exhibitionHallPerM2'
""")
Double findRentalRatePerM2();
/** 행사 부스 총 판매면적(㎡)·부스 수. */
@Select("""
SELECT COALESCE(sum(ST_Area(b.geom)), 0) AS salesArea,
count(*) AS boothCount
FROM booth b JOIN layout l ON l.id = b.layout_id
WHERE l.event_id = #{eventId}
""")
Map<String, Object> findAreaAndCount(@Param("eventId") String eventId);
/** 월별 부스 생성 추이(period=YYYY-MM, 면적·부스 수). days&gt;0 이면 최근 N일로 제한. */
@Select("""
<script>
SELECT to_char(b.created_at, 'YYYY-MM') AS period,
COALESCE(sum(ST_Area(b.geom)), 0) AS salesArea,
count(*) AS booths
FROM booth b JOIN layout l ON l.id = b.layout_id
WHERE l.event_id = #{eventId}
<if test="days > 0">AND b.created_at >= now() - make_interval(days => #{days})</if>
GROUP BY to_char(b.created_at, 'YYYY-MM')
ORDER BY period
</script>
""")
List<Map<String, Object>> findMonthlyTrend(@Param("eventId") String eventId, @Param("days") int days);
/** 유틸리티 매출 — quote.total 합계(숫자 형태만, 없으면 0). jsonb {@code ?} 연산자는 JDBC 파라미터와 충돌하므로 회피. */
@Select("""
SELECT COALESCE(sum((quote->>'total')::numeric), 0)
FROM utility_order
WHERE event_id = #{eventId} AND quote->>'total' ~ '^[0-9]+(\\.[0-9]+)?$'
""")
Double findUtilityRevenue(@Param("eventId") String eventId);
}

View File

@ -1,128 +0,0 @@
package com.zioinfo.kintex.analytics;
import com.zioinfo.kintex.analytics.dto.AnalyticsDto;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 경영분석 BI 서비스 판매면적×임대 요율로 매출을 산출하고 부스/유틸리티 실적을 조립.
* perspective(operator|exhibitor) 따라 필드 세트가 갈린다. 원천 부재 지표는 빈배열(정직).
*/
@Service
public class AnalyticsService {
private final AnalyticsMapper mapper;
public AnalyticsService(AnalyticsMapper mapper) {
this.mapper = mapper;
}
public AnalyticsDto getAnalytics(String eventId, String perspective, String period) {
boolean exhibitor = "exhibitor".equalsIgnoreCase(perspective);
int days = periodDays(period);
Double rate = mapper.findRentalRatePerM2();
double ratePerM2 = rate == null ? 0.0 : rate;
Map<String, Object> ac = mapper.findAreaAndCount(eventId);
double salesArea = dbl(ac == null ? null : ac.get("salesArea"));
long boothCount = lng(ac == null ? null : ac.get("boothCount"));
long rentalRevenue = Math.round(salesArea * ratePerM2);
long utilityRevenue = Math.round(dbl(mapper.findUtilityRevenue(eventId)));
List<AnalyticsDto.Kpi> kpis = new ArrayList<>();
kpis.add(new AnalyticsDto.Kpi("임대 매출", formatKrw(rentalRevenue), null, "flat"));
kpis.add(new AnalyticsDto.Kpi("유틸리티 매출", formatKrw(utilityRevenue), null, "flat"));
kpis.add(new AnalyticsDto.Kpi("판매 부스", String.valueOf(boothCount), null, "flat"));
kpis.add(new AnalyticsDto.Kpi("판매 면적", Math.round(salesArea) + "", null, "flat"));
List<AnalyticsDto.TrendPoint> trend = new ArrayList<>();
List<Map<String, Object>> trendRows = mapper.findMonthlyTrend(eventId, days);
if (trendRows != null) {
for (Map<String, Object> r : trendRows) {
long area = Math.round(dbl(r.get("salesArea")));
trend.add(new AnalyticsDto.TrendPoint(
str(r.get("period")), Math.round(area * ratePerM2), lng(r.get("booths"))));
}
}
// 손익(operator 관점) 매출 항목만. 업종 분류 원천 부재 sectors 미집계.
List<AnalyticsDto.PnlRow> pnl = new ArrayList<>();
List<AnalyticsDto.Sector> sectors = new ArrayList<>();
if (!exhibitor) {
pnl.add(new AnalyticsDto.PnlRow("부스 임대", rentalRevenue, grade(rentalRevenue)));
pnl.add(new AnalyticsDto.PnlRow("유틸리티", utilityRevenue, grade(utilityRevenue)));
}
// 참가업체 성과 방문자 리드(M13) 원천 부재 미집계(빈배열).
List<AnalyticsDto.ExhibitorPerf> exhibitorPerf = new ArrayList<>();
return new AnalyticsDto(kpis, trend, sectors, pnl, exhibitorPerf);
}
private static int periodDays(String period) {
if (period == null) {
return 0;
}
return switch (period.toLowerCase()) {
case "d90" -> 90;
case "annual" -> 365;
default -> 0; // event = 전체 기간
};
}
/** 금액 등급 — 억 단위 임계(단순·결정적). 원천 임계 룰셋 확정 시 교체. */
private static String grade(long amount) {
if (amount >= 1_000_000_000L) return "A";
if (amount >= 100_000_000L) return "B";
return "C";
}
/** 원화 축약 표기(억/만). */
private static String formatKrw(long won) {
if (won >= 100_000_000L) {
double eok = won / 100_000_000.0;
return "" + trim(eok) + "";
}
if (won >= 10_000L) {
double man = won / 10_000.0;
return "" + trim(man) + "";
}
return "" + won;
}
private static String trim(double v) {
double r = Math.round(v * 100.0) / 100.0;
if (r == Math.floor(r)) {
return String.valueOf((long) r);
}
return String.valueOf(r);
}
private static double dbl(Object o) {
if (o == null) return 0.0;
if (o instanceof Number n) return n.doubleValue();
try {
return Double.parseDouble(String.valueOf(o));
} catch (NumberFormatException e) {
return 0.0;
}
}
private static long lng(Object o) {
if (o == null) return 0L;
if (o instanceof Number n) return n.longValue();
try {
return Long.parseLong(String.valueOf(o));
} catch (NumberFormatException e) {
return 0L;
}
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -1,35 +0,0 @@
package com.zioinfo.kintex.analytics.dto;
import java.util.List;
/**
* 경영분석 BI 집계 (SCR-13 / 06_backend_api_gaps §3).
* 집계원: booth 면적(ST_Area)×임대 요율(master_data)·utility_order.quote·booth 월별 추이.
* 업종(sectors)·리드(exhibitorPerf) 원천(M13 방문자/업종 분류) 부재 빈배열(정직 반환).
*/
public record AnalyticsDto(
List<Kpi> kpis,
List<TrendPoint> trend,
List<Sector> sectors,
List<PnlRow> pnl,
List<ExhibitorPerf> exhibitorPerf
) {
public record Kpi(String label, String value, String delta, String trend) {
}
/** 월별 매출·부스 추이(period="YYYY-MM"). */
public record TrendPoint(String period, long revenue, long booths) {
}
/** 업종 구성(원천 부재 시 미집계). */
public record Sector(String name, double sharePercent, long revenue) {
}
/** 손익 항목(grade: A|B|C, 금액 기준 등급). */
public record PnlRow(String item, long amount, String grade) {
}
/** 참가업체 성과(리드·전환율 — M13 연동 필요). */
public record ExhibitorPerf(String companyName, long leads, double conversionPercent) {
}
}

View File

@ -1,47 +0,0 @@
package com.zioinfo.kintex.catalog;
import com.zioinfo.kintex.auth.EventAccessGuard;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.catalog.dto.ExhibitionDto;
import com.zioinfo.kintex.common.ApiResponse;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 전시 일정 카탈로그 API (SCR-15).
* 인증 필수(로그인 사용자면 누구나) 워크스페이스( 행사) 달리 10년치 카탈로그를 제공한다.
* <p>배열 직접 반환({@code ApiResponse<List<ExhibitionDto>>}) 프론트 {@code exhibitionApi.list()} 계약 정합.
*/
@RestController
@RequestMapping("/api/events")
public class EventCatalogController {
/** 카탈로그 상한(과도 응답 방지) — V10 시드 1,098건을 단일 응답으로 커버. */
private static final int MAX_LIMIT = 2000;
private final EventCatalogService service;
private final EventAccessGuard guard;
public EventCatalogController(EventCatalogService service, EventAccessGuard guard) {
this.service = service;
this.guard = guard;
}
/** GET /api/events?year=&status=ongoing|upcoming|ended — 필터 없으면 전체(시작일 내림차순). */
@GetMapping
public ApiResponse<List<ExhibitionDto>> list(@AuthenticationPrincipal KintexPrincipal principal,
@RequestParam(required = false) String year,
@RequestParam(required = false) String status,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "2000") int size) {
guard.require(principal);
int limit = Math.min(Math.max(size, 1), MAX_LIMIT);
int offset = Math.max(page, 0) * limit;
return ApiResponse.ok(service.list(year, status, limit, offset));
}
}

View File

@ -1,73 +0,0 @@
package com.zioinfo.kintex.catalog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 전시 일정 카탈로그 매퍼 {@code event} 테이블 read-only 조회(V10 시드 1,098건).
* <p>hallLabel {@code hall_assignment}(대표 우선) {@code hall.label} 조인으로 enrich(없으면 null).
* category·estVisitors 원천 컬럼 부재 항상 null(후속 트랙 enrich). 마이그레이션을 변경하지 않는다.
*
* <p>status 필터는 DB 원문(active/ended) 계약 (ongoing|upcoming|ended) 다르므로 <b>날짜 기준</b>으로 판정한다
* 프론트의 재계산 규칙과 동일: ended=end&lt;오늘, ongoing=시작오늘종료, upcoming=시작&gt;오늘.
*/
@Mapper
public interface EventCatalogMapper {
/** 카탈로그 조회 — year(시작연도)·status(날짜 기준) 선택 필터. 시작일 내림차순. */
@Select("""
<script>
SELECT e.id,
e.name,
to_char(e.start_date, 'YYYY-MM-DD') AS startDate,
to_char(e.end_date, 'YYYY-MM-DD') AS endDate,
e.status,
h.label AS hallLabel,
CAST(NULL AS varchar) AS category,
CAST(NULL AS integer) AS estVisitors
FROM event e
LEFT JOIN LATERAL (
SELECT ha.hall_id
FROM hall_assignment ha
WHERE ha.event_id = e.id
ORDER BY ha.is_primary DESC
LIMIT 1
) pa ON true
LEFT JOIN hall h ON h.id = pa.hall_id
<where>
<if test="year != null and year != ''">
AND extract(year FROM e.start_date) = CAST(#{year} AS integer)
</if>
<if test="status == 'ended'">AND e.end_date &lt; CURRENT_DATE</if>
<if test="status == 'ongoing'">AND e.start_date &lt;= CURRENT_DATE AND e.end_date &gt;= CURRENT_DATE</if>
<if test="status == 'upcoming'">AND e.start_date &gt; CURRENT_DATE</if>
</where>
ORDER BY e.start_date DESC NULLS LAST, e.id DESC
LIMIT #{limit} OFFSET #{offset}
</script>
""")
List<Map<String, Object>> findEvents(@Param("year") String year,
@Param("status") String status,
@Param("limit") int limit,
@Param("offset") int offset);
/** 필터 조건 총 건수(페이징 메타). */
@Select("""
<script>
SELECT count(*) FROM event e
<where>
<if test="year != null and year != ''">
AND extract(year FROM e.start_date) = CAST(#{year} AS integer)
</if>
<if test="status == 'ended'">AND e.end_date &lt; CURRENT_DATE</if>
<if test="status == 'ongoing'">AND e.start_date &lt;= CURRENT_DATE AND e.end_date &gt;= CURRENT_DATE</if>
<if test="status == 'upcoming'">AND e.start_date &gt; CURRENT_DATE</if>
</where>
</script>
""")
long countEvents(@Param("year") String year, @Param("status") String status);
}

View File

@ -1,12 +0,0 @@
package com.zioinfo.kintex.catalog;
import com.zioinfo.kintex.catalog.dto.ExhibitionDto;
import java.util.List;
/** 전시 일정 카탈로그 서비스 — 전 홀 10년치 행사 목록(멤버십 무관 조회). */
public interface EventCatalogService {
/** year(시작연도)·status(ongoing|upcoming|ended, 날짜 기준) 선택 필터. 시작일 내림차순. */
List<ExhibitionDto> list(String year, String status, int limit, int offset);
}

View File

@ -1,52 +0,0 @@
package com.zioinfo.kintex.catalog;
import com.zioinfo.kintex.catalog.dto.ExhibitionDto;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** 전시 일정 카탈로그 서비스 구현 — {@link EventCatalogMapper} 조회 결과를 DTO로 매핑. */
@Service
public class EventCatalogServiceImpl implements EventCatalogService {
private final EventCatalogMapper mapper;
public EventCatalogServiceImpl(EventCatalogMapper mapper) {
this.mapper = mapper;
}
@Override
public List<ExhibitionDto> list(String year, String status, int limit, int offset) {
List<Map<String, Object>> rows = mapper.findEvents(nullIfBlank(year), nullIfBlank(status), limit, offset);
List<ExhibitionDto> out = new ArrayList<>(rows == null ? 0 : rows.size());
if (rows != null) {
for (Map<String, Object> r : rows) {
out.add(new ExhibitionDto(
str(r.get("id")), str(r.get("name")),
str(r.get("startDate")), str(r.get("endDate")), str(r.get("status")),
str(r.get("hallLabel")), str(r.get("category")), intOrNull(r.get("estVisitors"))));
}
}
return out;
}
private static String nullIfBlank(String s) {
return s == null || s.isBlank() || "all".equalsIgnoreCase(s) ? null : s;
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
private static Integer intOrNull(Object o) {
if (o == null) return null;
if (o instanceof Number n) return n.intValue();
try {
return Integer.valueOf(String.valueOf(o));
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@ -1,18 +0,0 @@
package com.zioinfo.kintex.catalog.dto;
/**
* 전시 일정 카탈로그 항목 (SCR-15 / 06_backend_api_gaps §1).
* 필수(DB 보유): id·name·startDate·endDate·status. 선택(enrich·nullable): hallLabel·category·estVisitors.
* <p>status {@code event.status} DB 원문(active/ended ) 그대로 반환 프론트가 날짜 기준 재계산한다.
*/
public record ExhibitionDto(
String id,
String name,
String startDate,
String endDate,
String status,
String hallLabel,
String category,
Integer estVisitors
) {
}

View File

@ -35,7 +35,6 @@ public class SecurityConfig {
.requestMatchers("/health", "/api/auth/login", "/ws/**", .requestMatchers("/health", "/api/auth/login", "/ws/**",
"/api/internal/render/callback", "/api/internal/render/callback",
"/api/auth/login/secure", "/api/auth/otp/verify", "/api/auth/login/secure", "/api/auth/otp/verify",
"/api/auth/otp/enroll/setup", "/api/auth/otp/enroll/verify",
"/api/auth/register", "/api/auth/password/forgot", "/api/auth/register", "/api/auth/password/forgot",
"/api/auth/password/reset", "/api/auth/password/reset",
// 공개 콘텐츠(로그인 슬라이드 ) + 업로드 정적 파일 // 공개 콘텐츠(로그인 슬라이드 ) + 업로드 정적 파일

View File

@ -1,35 +0,0 @@
package com.zioinfo.kintex.dashboard;
import com.zioinfo.kintex.auth.EventAccessGuard;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.ApiResponse;
import com.zioinfo.kintex.dashboard.dto.DashboardDto;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 주최자 대시보드 API (SCR-02). 행사 멤버/홀매니저 열람.
*/
@RestController
@RequestMapping("/api/events/{eventId}/dashboard")
public class DashboardController {
private final DashboardService service;
private final EventAccessGuard guard;
public DashboardController(DashboardService service, EventAccessGuard guard) {
this.service = service;
this.guard = guard;
}
/** GET — 마일스톤·KPI·참가업체·활동 피드 집계. */
@GetMapping
public ApiResponse<DashboardDto> get(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String eventId) {
guard.requireEventAccess(principal, eventId);
return ApiResponse.ok(service.getDashboard(eventId));
}
}

View File

@ -1,73 +0,0 @@
package com.zioinfo.kintex.dashboard;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 주최자 대시보드 집계 매퍼 event·booth·design_plan·utility_order·render_job read-only.
* booth {@code layout.event_id} 경유로 행사에 귀속된다. 마이그레이션 불변.
*/
@Mapper
public interface DashboardMapper {
/** 행사 헤더(마일스톤 파생용 시작/종료일). 없으면 null. */
@Select("""
SELECT to_char(start_date, 'YYYY-MM-DD') AS startDate,
to_char(end_date, 'YYYY-MM-DD') AS endDate,
name
FROM event WHERE id = #{eventId}
""")
Map<String, Object> findEventHeader(@Param("eventId") String eventId);
/** KPI 원자 집계(단일 행) — 부스 수·목표 부스·설계 승인·유틸리티 신청·렌더 완료. */
@Select("""
SELECT
(SELECT count(*) FROM booth b JOIN layout l ON l.id = b.layout_id
WHERE l.event_id = #{eventId}) AS boothCount,
(SELECT COALESCE(sum(h.booth_capacity), 0) FROM hall_assignment ha
JOIN hall h ON h.id = ha.hall_id WHERE ha.event_id = #{eventId}) AS targetBooths,
(SELECT count(*) FROM booth b JOIN layout l ON l.id = b.layout_id
JOIN design_plan dp ON dp.booth_id = b.id
WHERE l.event_id = #{eventId} AND dp.status = 'approved') AS designApproved,
(SELECT count(*) FROM design_plan dp JOIN booth b ON b.id = dp.booth_id
JOIN layout l ON l.id = b.layout_id WHERE l.event_id = #{eventId}) AS designTotal,
(SELECT count(*) FROM utility_order u WHERE u.event_id = #{eventId}) AS utilityOrders,
(SELECT count(*) FROM render_job r WHERE r.event_id = #{eventId} AND r.status = 'DONE') AS renderDone
""")
Map<String, Object> findKpiCounts(@Param("eventId") String eventId);
/** 참가업체 부스 현황(최신 설계안 상태 조인). 최대 100건. */
@Select("""
SELECT b.booth_no AS boothNo,
b.assigned_company_name AS companyName,
COALESCE(dp.status, 'draft') AS status
FROM booth b
JOIN layout l ON l.id = b.layout_id
LEFT JOIN LATERAL (
SELECT status FROM design_plan d
WHERE d.booth_id = b.id ORDER BY version DESC LIMIT 1
) dp ON true
WHERE l.event_id = #{eventId}
ORDER BY b.booth_no NULLS LAST
LIMIT 100
""")
List<Map<String, Object>> findExhibitors(@Param("eventId") String eventId);
/** 활동 피드 — 렌더잡 최근 이력(승인/위반 원천 부재 시 render 만). 최대 12건. */
@Select("""
SELECT r.id,
r.status,
r.shot_preset AS shotPreset,
r.booth_id AS boothId,
to_char(r.updated_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS at
FROM render_job r
WHERE r.event_id = #{eventId}
ORDER BY r.updated_at DESC
LIMIT 12
""")
List<Map<String, Object>> findRecentRenders(@Param("eventId") String eventId);
}

View File

@ -1,155 +0,0 @@
package com.zioinfo.kintex.dashboard;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.dashboard.dto.DashboardDto;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 주최자 대시보드 서비스 테이블 집계로 KPI·참가업체·피드를 조립.
* 마일스톤은 행사 시작일 기준 표준 일정을 파생(state 오늘 기준 판정) 원천 값이 아닌 파생 스케줄.
*/
@Service
public class DashboardService {
/** 시작일 기준 상대 마일스톤(라벨, 시작일로부터 -일수). 개장은 0. */
private static final Object[][] MILESTONE_TEMPLATE = {
{"홀 배정", 150},
{"부스 배치 확정", 60},
{"유틸리티 신청 마감", 25},
{"신고서류 제출", 7},
{"개장", 0},
};
private final DashboardMapper mapper;
public DashboardService(DashboardMapper mapper) {
this.mapper = mapper;
}
public DashboardDto getDashboard(String eventId) {
Map<String, Object> header = mapper.findEventHeader(eventId);
if (header == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "행사를 찾을 수 없습니다.");
}
return new DashboardDto(
buildMilestones(str(header.get("startDate"))),
buildKpis(mapper.findKpiCounts(eventId)),
buildExhibitors(mapper.findExhibitors(eventId)),
buildFeed(mapper.findRecentRenders(eventId)));
}
/** 행사 시작일에서 상대 일정을 파생. 시작일이 없으면 빈 목록(정직). */
private List<DashboardDto.Milestone> buildMilestones(String startDate) {
List<DashboardDto.Milestone> out = new ArrayList<>();
LocalDate start = parseDate(startDate);
if (start == null) {
return out;
}
LocalDate today = LocalDate.now();
for (Object[] tpl : MILESTONE_TEMPLATE) {
String label = (String) tpl[0];
int daysBefore = (int) tpl[1];
LocalDate date = start.minusDays(daysBefore);
String state;
if (date.isBefore(today)) {
state = "done";
} else if (date.isEqual(today)) {
state = "current";
} else {
state = "upcoming";
}
out.add(new DashboardDto.Milestone(label, date.toString(), state));
}
return out;
}
private List<DashboardDto.Kpi> buildKpis(Map<String, Object> c) {
List<DashboardDto.Kpi> out = new ArrayList<>();
if (c == null) {
return out;
}
long boothCount = lng(c.get("boothCount"));
long targetBooths = lng(c.get("targetBooths"));
long designApproved = lng(c.get("designApproved"));
long designTotal = lng(c.get("designTotal"));
long utilityOrders = lng(c.get("utilityOrders"));
long renderDone = lng(c.get("renderDone"));
String boothValue = targetBooths > 0 ? boothCount + "/" + targetBooths : String.valueOf(boothCount);
out.add(new DashboardDto.Kpi("부스 배치", boothValue, null, "flat"));
out.add(new DashboardDto.Kpi("설계 승인", designApproved + "/" + designTotal, null, "flat"));
out.add(new DashboardDto.Kpi("유틸리티 신청", String.valueOf(utilityOrders), null, "flat"));
out.add(new DashboardDto.Kpi("렌더 완료", String.valueOf(renderDone), null, "flat"));
return out;
}
private List<DashboardDto.Exhibitor> buildExhibitors(List<Map<String, Object>> rows) {
List<DashboardDto.Exhibitor> out = new ArrayList<>();
if (rows == null) {
return out;
}
for (Map<String, Object> r : rows) {
out.add(new DashboardDto.Exhibitor(
str(r.get("companyName")), str(r.get("boothNo")), str(r.get("status"))));
}
return out;
}
private List<DashboardDto.Feed> buildFeed(List<Map<String, Object>> rows) {
List<DashboardDto.Feed> out = new ArrayList<>();
if (rows == null) {
return out;
}
for (Map<String, Object> r : rows) {
String status = str(r.get("status"));
String preset = str(r.get("shotPreset"));
String message = "이미지 생성(" + preset + ") " + renderStatusLabel(status);
out.add(new DashboardDto.Feed(str(r.get("id")), "render", message, str(r.get("at"))));
}
return out;
}
private static String renderStatusLabel(String status) {
if (status == null) {
return "";
}
return switch (status) {
case "DONE" -> "완료";
case "FAILED" -> "실패";
case "RUNNING" -> "진행 중";
default -> "대기";
};
}
private static LocalDate parseDate(String s) {
if (s == null || s.isBlank()) {
return null;
}
try {
return LocalDate.parse(s);
} catch (DateTimeParseException e) {
return null;
}
}
private static long lng(Object o) {
if (o == null) return 0L;
if (o instanceof Number n) return n.longValue();
try {
return Long.parseLong(String.valueOf(o));
} catch (NumberFormatException e) {
return 0L;
}
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -1,30 +0,0 @@
package com.zioinfo.kintex.dashboard.dto;
import java.util.List;
/**
* 주최자 대시보드 집계 (SCR-02 / 06_backend_api_gaps §2).
* 집계원: event·booth(layout)·design_plan·utility_order·render_job. 데이터 없으면 0/빈배열(정직 반환).
*/
public record DashboardDto(
List<Milestone> milestones,
List<Kpi> kpis,
List<Exhibitor> exhibitors,
List<Feed> feed
) {
/** 마일스톤 — 행사 시작일 기준 파생 일정(state 는 오늘 기준 판정). */
public record Milestone(String label, String date, String state) {
}
/** KPI — 이력 부재 시 delta=null·trend=flat. */
public record Kpi(String label, String value, String delta, String trend) {
}
/** 참가업체 부스 현황(booth + 최신 설계안 상태). */
public record Exhibitor(String companyName, String boothNo, String status) {
}
/** 활동 피드(렌더/승인/위반) — 현재 render_job 이력 원천. */
public record Feed(String id, String kind, String message, String at) {
}
}

View File

@ -150,9 +150,6 @@ public class FloorplanServiceImpl implements FloorplanService {
Integer exitsBlocked = boothMapper.countExitsBlocked(layoutId, hallId); Integer exitsBlocked = boothMapper.countExitsBlocked(layoutId, hallId);
metrics.put("layout.exits_blocked_count", exitsBlocked == null ? 0 : exitsBlocked); metrics.put("layout.exits_blocked_count", exitsBlocked == null ? 0 : exitsBlocked);
Integer overlaps = boothMapper.countOverlappingBooths(layoutId);
metrics.put("layout.overlap_count", overlaps == null ? 0 : overlaps);
BoothDto maxHeight = maxBy(booths, BoothDto::heightM); BoothDto maxHeight = maxBy(booths, BoothDto::heightM);
if (maxHeight != null && maxHeight.heightM() != null) { if (maxHeight != null && maxHeight.heightM() != null) {
metrics.put("booth.height_m", maxHeight.heightM()); metrics.put("booth.height_m", maxHeight.heightM());

View File

@ -38,10 +38,4 @@ public interface BoothMapper {
/** 비상구를 차단하는 부스 수 — ST_Intersects(booth, exit_buffer). */ /** 비상구를 차단하는 부스 수 — ST_Intersects(booth, exit_buffer). */
Integer countExitsBlocked(@Param("layoutId") String layoutId, Integer countExitsBlocked(@Param("layoutId") String layoutId,
@Param("hallId") String hallId); @Param("hallId") String hallId);
/**
* 폴리곤 내부가 겹치는 부스 쌍의 부스는 서로 겹칠 없다(배치 무결성).
* ST_Overlaps(경계 교차) 아니라 포함·동일도 무효이므로 "내부 교차(ST_Intersects AND NOT ST_Touches)" 판정한다.
*/
Integer countOverlappingBooths(@Param("layoutId") String layoutId);
} }

View File

@ -1,36 +0,0 @@
package com.zioinfo.kintex.ops;
import com.zioinfo.kintex.auth.EventAccessGuard;
import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.ApiResponse;
import com.zioinfo.kintex.ops.dto.OpsDto;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 현장 운영 API (SCR-16). 홀매니저/주최자 열람.
*/
@RestController
@RequestMapping("/api/events/{eventId}/ops")
public class OpsController {
private final OpsService service;
private final EventAccessGuard guard;
public OpsController(OpsService service, EventAccessGuard guard) {
this.service = service;
this.guard = guard;
}
/** GET — 홀 혼잡·HVAC·조명·주차 현황(센서 미연동 지표는 0/null). */
@GetMapping
public ApiResponse<OpsDto> get(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String eventId) {
guard.requireRole(principal, eventId, EventRole.ORGANIZER, EventRole.HALL_MANAGER);
return ApiResponse.ok(service.getOps(eventId));
}
}

View File

@ -1,26 +0,0 @@
package com.zioinfo.kintex.ops;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 현장 운영 매퍼 hall_assignment·hall read-only(행사에 배정된 목록).
* 혼잡/HVAC/조명/주차 센서 원천이 없으므로 목록만 조회한다. 마이그레이션 불변.
*/
@Mapper
public interface OpsMapper {
/** 행사 배정 홀 목록(대표 홀 우선 정렬). */
@Select("""
SELECT h.id AS hallId, h.label
FROM hall_assignment ha
JOIN hall h ON h.id = ha.hall_id
WHERE ha.event_id = #{eventId}
ORDER BY ha.is_primary DESC, h.id
""")
List<Map<String, Object>> findAssignedHalls(@Param("eventId") String eventId);
}

View File

@ -1,39 +0,0 @@
package com.zioinfo.kintex.ops;
import com.zioinfo.kintex.ops.dto.OpsDto;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 현장 운영 서비스 배정 목록만 조회. 혼잡/HVAC/조명/주차는 센서 부재 0/null/빈배열(정직).
*/
@Service
public class OpsService {
private final OpsMapper mapper;
public OpsService(OpsMapper mapper) {
this.mapper = mapper;
}
public OpsDto getOps(String eventId) {
List<OpsDto.Hall> halls = new ArrayList<>();
List<Map<String, Object>> rows = mapper.findAssignedHalls(eventId);
if (rows != null) {
for (Map<String, Object> r : rows) {
// 혼잡 센서(M14) 미연동 estPeople=0, heat=smooth.
halls.add(new OpsDto.Hall(str(r.get("hallId")), str(r.get("label")), 0L, "smooth"));
}
}
// HVAC/조명/주차 센서·IoT 미연동 null/빈배열.
OpsDto.Hvac hvac = new OpsDto.Hvac(null, null, null);
return new OpsDto(halls, hvac, new ArrayList<>(), new ArrayList<>());
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -1,31 +0,0 @@
package com.zioinfo.kintex.ops.dto;
import java.util.List;
/**
* 현장 운영 현황 (SCR-16 / 06_backend_api_gaps §5).
* 집계원: hall_assignment·hall( 목록). 혼잡/HVAC/조명/주차는 센서·IoT 미연동(PLANNING M14) 0/null/빈배열(정직).
* <p>하드웨어 연동 코드 없음 표시 전용. 실시간성 필요 WebSocket /topic/ops/{eventId} 확장 검토(현재 폴링 GET).
*/
public record OpsDto(
List<Hall> halls,
Hvac hvac,
List<LightingZone> lighting,
List<ParkingZone> parking
) {
/** 홀 혼잡(estPeople·heat) — 센서 부재 시 0/smooth. */
public record Hall(String hallId, String label, long estPeople, String heat) {
}
/** 공조 계측(센서 부재 시 null). */
public record Hvac(Double tempC, Integer humidityPercent, Integer co2Ppm) {
}
/** 조명 존 점등률. */
public record LightingZone(String zone, int onPercent) {
}
/** 주차 존 점유·수용. */
public record ParkingZone(String zone, int occupancy, int capacity) {
}
}

View File

@ -37,13 +37,12 @@ public interface AccountSecurityMapper {
+ "verify_method = 'OTP', updated_at = now() WHERE id = #{userId}") + "verify_method = 'OTP', updated_at = now() WHERE id = #{userId}")
int saveOtpSecret(@Param("userId") String userId, @Param("secret") String secret); int saveOtpSecret(@Param("userId") String userId, @Param("secret") String secret);
/** OTP 활성(코드 확인 완료) — 최초 활성 시각 기록. */ /** OTP 활성(코드 확인 완료). */
@Update("UPDATE app_user SET otp_enabled = true, verify_method = 'OTP', " @Update("UPDATE app_user SET otp_enabled = true, updated_at = now() WHERE id = #{userId}")
+ "otp_verified_at = COALESCE(otp_verified_at, now()), updated_at = now() WHERE id = #{userId}")
int enableOtp(@Param("userId") String userId); int enableOtp(@Param("userId") String userId);
/** OTP 초기화(비활성 + 시크릿 폐기 + 검증시각 초기화) — 본인 해제/관리자 리셋. */ /** OTP 초기화(비활성 + 시크릿 폐기) — 본인 해제/관리자 리셋. */
@Update("UPDATE app_user SET otp_secret = NULL, otp_enabled = false, otp_verified_at = NULL, " @Update("UPDATE app_user SET otp_secret = NULL, otp_enabled = false, "
+ "verify_method = 'EMAIL', updated_at = now() WHERE id = #{userId}") + "verify_method = 'EMAIL', updated_at = now() WHERE id = #{userId}")
int resetOtp(@Param("userId") String userId); int resetOtp(@Param("userId") String userId);

View File

@ -31,18 +31,6 @@ public class OtpChallengeStore {
return token; return token;
} }
/** 토큰 조회(비소비) → userId. 만료/무효면 null. 등록(enroll) setup 단계에서 사용. */
public String peek(String token) {
if (token == null) {
return null;
}
Entry e = store.get(token);
if (e == null || e.expiresAt() < Instant.now().toEpochMilli()) {
return null;
}
return e.userId();
}
/** 토큰 소비(1회성) → userId. 만료/무효면 null. */ /** 토큰 소비(1회성) → userId. 만료/무효면 null. */
public String consume(String token) { public String consume(String token) {
if (token == null) { if (token == null) {

View File

@ -1,97 +0,0 @@
package com.zioinfo.kintex.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
/**
* OTP 시크릿 저장용 AES-256-GCM 봉인/해제(계약 §0-3: otp_secret 암호화 저장 필수).
* <p>포맷 {@code enc:v1:base64(iv(12) || ciphertext || tag(16))}. 접두어가 없으면 평문으로 간주해
* 그대로 반환한다(암호화 도입 이전 시크릿과의 하위호환·점진 이관). 키는 env 주입만
* ({@code KINTEX_OTP_ENC_KEY}, base64 32바이트). 미주입 개발 전용 파생키를 쓰되 경고 로그를 남긴다.
* ·평문 시크릿은 로그/응답에 절대 남기지 않는다.
*/
@Component
public class SecretCipher {
private static final Logger log = LoggerFactory.getLogger(SecretCipher.class);
private static final String PREFIX = "enc:v1:";
private static final String TRANSFORM = "AES/GCM/NoPadding";
private static final int IV_LEN = 12;
private static final int TAG_BITS = 128;
private final SecureRandom random = new SecureRandom();
private final SecretKeySpec key;
public SecretCipher(SecurityPolicyProperties props) {
this.key = resolveKey(props.getOtp().getEncKey());
}
/** 평문 시크릿을 봉인(enc:v1:...). null/blank 는 그대로 반환. */
public String seal(String plain) {
if (plain == null || plain.isBlank()) {
return plain;
}
try {
byte[] iv = new byte[IV_LEN];
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance(TRANSFORM);
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
byte[] ct = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
byte[] out = new byte[iv.length + ct.length];
System.arraycopy(iv, 0, out, 0, iv.length);
System.arraycopy(ct, 0, out, iv.length, ct.length);
return PREFIX + Base64.getEncoder().encodeToString(out);
} catch (Exception e) {
throw new IllegalStateException("OTP 시크릿 암호화 실패", e);
}
}
/** 봉인 시크릿을 해제. 접두어가 없으면 평문으로 간주(하위호환). null 은 null. */
public String open(String stored) {
if (stored == null || stored.isBlank()) {
return stored;
}
if (!stored.startsWith(PREFIX)) {
return stored; // 이관 이전 평문
}
try {
byte[] raw = Base64.getDecoder().decode(stored.substring(PREFIX.length()));
byte[] iv = new byte[IV_LEN];
System.arraycopy(raw, 0, iv, 0, IV_LEN);
Cipher cipher = Cipher.getInstance(TRANSFORM);
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
byte[] pt = cipher.doFinal(raw, IV_LEN, raw.length - IV_LEN);
return new String(pt, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new IllegalStateException("OTP 시크릿 복호화 실패", e);
}
}
private SecretKeySpec resolveKey(String configured) {
if (configured != null && !configured.isBlank()) {
byte[] k = Base64.getDecoder().decode(configured.trim());
if (k.length != 32) {
throw new IllegalStateException("KINTEX_OTP_ENC_KEY 는 base64 32바이트(AES-256)여야 합니다.");
}
return new SecretKeySpec(k, "AES");
}
// 개발 전용 파생키 운영에서는 반드시 env 주입. 값은 로그에 남기지 않는다.
log.warn("KINTEX_OTP_ENC_KEY 미주입 → 개발 전용 파생키 사용(운영 배포 전 반드시 주입 필요)");
try {
byte[] k = MessageDigest.getInstance("SHA-256")
.digest("kintex-dev-otp-key-do-not-use-in-prod".getBytes(StandardCharsets.UTF_8));
return new SecretKeySpec(k, "AES");
} catch (Exception e) {
throw new IllegalStateException("개발 파생키 생성 실패", e);
}
}
}

View File

@ -31,16 +31,8 @@ public class SecurityPolicyProperties {
public static class Otp { public static class Otp {
/** TOTP 발급자 표기(Authenticator 앱 라벨). */ /** TOTP 발급자 표기(Authenticator 앱 라벨). */
private String issuer = "KINTEX"; private String issuer = "KINTEX";
/** OTP 시크릿 암호화 키(base64 32바이트, AES-256). env 주입만 — 미주입 시 개발 파생키. */
private String encKey;
/** 필수 역할(관람객 외)에 OTP 등록·검증 강제. 운영 true 권장, 데모 편의 시 false. */
private boolean enforce = true;
public String getIssuer() { return issuer; } public String getIssuer() { return issuer; }
public void setIssuer(String v) { this.issuer = v; } public void setIssuer(String v) { this.issuer = v; }
public String getEncKey() { return encKey; }
public void setEncKey(String v) { this.encKey = v; }
public boolean isEnforce() { return enforce; }
public void setEnforce(boolean v) { this.enforce = v; }
} }
public static class AdminSeed { public static class AdminSeed {

View File

@ -5,7 +5,6 @@ import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.auth.dto.LoginResponse; import com.zioinfo.kintex.auth.dto.LoginResponse;
import com.zioinfo.kintex.common.ApiResponse; import com.zioinfo.kintex.common.ApiResponse;
import com.zioinfo.kintex.security.dto.OtpConfirmRequest; import com.zioinfo.kintex.security.dto.OtpConfirmRequest;
import com.zioinfo.kintex.security.dto.OtpEnrollRequest;
import com.zioinfo.kintex.security.dto.OtpSetupResponse; import com.zioinfo.kintex.security.dto.OtpSetupResponse;
import com.zioinfo.kintex.security.dto.OtpStatusResponse; import com.zioinfo.kintex.security.dto.OtpStatusResponse;
import com.zioinfo.kintex.security.dto.OtpVerifyRequest; import com.zioinfo.kintex.security.dto.OtpVerifyRequest;
@ -41,24 +40,12 @@ public class TwoFactorController {
return ApiResponse.ok(service.secureLogin(req.email(), req.password())); return ApiResponse.ok(service.secureLogin(req.email(), req.password()));
} }
/** 공개 — 2단계 OTP 검증(등록 사용자) → 최종 토큰. */ /** 공개 — 2단계 OTP 검증 → 최종 토큰. */
@PostMapping("/otp/verify") @PostMapping("/otp/verify")
public ApiResponse<LoginResponse> verifyOtp(@Valid @RequestBody OtpVerifyRequest req) { public ApiResponse<LoginResponse> verifyOtp(@Valid @RequestBody OtpVerifyRequest req) {
return ApiResponse.ok(service.verifyOtp(req.challengeToken(), req.code())); return ApiResponse.ok(service.verifyOtp(req.challengeToken(), req.code()));
} }
/** 공개(프리오스) — 필수 역할 등록 강제: setup(시크릿·QR 발급). challengeToken 필요. */
@PostMapping("/otp/enroll/setup")
public ApiResponse<OtpSetupResponse> enrollSetup(@Valid @RequestBody OtpEnrollRequest req) {
return ApiResponse.ok(service.enrollSetup(req.challengeToken()));
}
/** 공개(프리오스) — 필수 역할 등록 강제: verify(코드 확인·활성화) → 최종 토큰. */
@PostMapping("/otp/enroll/verify")
public ApiResponse<LoginResponse> enrollVerify(@Valid @RequestBody OtpVerifyRequest req) {
return ApiResponse.ok(service.enrollVerify(req.challengeToken(), req.code()));
}
/** 인증 — OTP 등록 시작(시크릿·QR 발급). */ /** 인증 — OTP 등록 시작(시크릿·QR 발급). */
@PostMapping("/otp/setup") @PostMapping("/otp/setup")
public ApiResponse<OtpSetupResponse> setup(@AuthenticationPrincipal KintexPrincipal principal) { public ApiResponse<OtpSetupResponse> setup(@AuthenticationPrincipal KintexPrincipal principal) {

View File

@ -34,14 +34,11 @@ public class TwoFactorService {
private final TotpService totpService; private final TotpService totpService;
private final LoginAttemptService loginAttempts; private final LoginAttemptService loginAttempts;
private final OtpChallengeStore challengeStore; private final OtpChallengeStore challengeStore;
private final SecretCipher secretCipher;
private final SecurityPolicyProperties props;
public TwoFactorService(AccountSecurityMapper securityMapper, UserMapper userMapper, public TwoFactorService(AccountSecurityMapper securityMapper, UserMapper userMapper,
JwtService jwtService, PasswordEncoder passwordEncoder, JwtService jwtService, PasswordEncoder passwordEncoder,
TotpService totpService, LoginAttemptService loginAttempts, TotpService totpService, LoginAttemptService loginAttempts,
OtpChallengeStore challengeStore, SecretCipher secretCipher, OtpChallengeStore challengeStore) {
SecurityPolicyProperties props) {
this.securityMapper = securityMapper; this.securityMapper = securityMapper;
this.userMapper = userMapper; this.userMapper = userMapper;
this.jwtService = jwtService; this.jwtService = jwtService;
@ -49,11 +46,9 @@ public class TwoFactorService {
this.totpService = totpService; this.totpService = totpService;
this.loginAttempts = loginAttempts; this.loginAttempts = loginAttempts;
this.challengeStore = challengeStore; this.challengeStore = challengeStore;
this.secretCipher = secretCipher;
this.props = props;
} }
/** 1단계: 잠금 확인 → 자격검증 → (OTP등록자=챌린지, 필수역할 미등록=등록강제, 그 외=토큰). */ /** 1단계: 잠금 확인 → 자격검증 → (OTP면 챌린지, 아니면 토큰). */
public SecureLoginResponse secureLogin(String email, String rawPassword) { public SecureLoginResponse secureLogin(String email, String rawPassword) {
Map<String, Object> row = securityMapper.findAuthSecurityByEmail(email); Map<String, Object> row = securityMapper.findAuthSecurityByEmail(email);
loginAttempts.assertNotLocked(row); loginAttempts.assertNotLocked(row);
@ -64,16 +59,11 @@ public class TwoFactorService {
} }
String userId = str(row.get("userId")); String userId = str(row.get("userId"));
loginAttempts.recordSuccess(userId); loginAttempts.recordSuccess(userId);
boolean hallManager = bool(row.get("hallManager"));
if (Boolean.TRUE.equals(bool(row.get("otpEnabled")))) { if (Boolean.TRUE.equals(bool(row.get("otpEnabled")))) {
return SecureLoginResponse.otpRequired(challengeStore.issue(userId)); return SecureLoginResponse.otpRequired(challengeStore.issue(userId));
} }
// 미등록 필수 역할이면 등록 강제(프리오스 챌린지), 관람객 등은 선택(즉시 토큰). return SecureLoginResponse.ok(buildLoginResponse(userId,
if (props.getOtp().isEnforce() str(row.get("displayName")), bool(row.get("hallManager"))));
&& requiresOtp(userId, hallManager, str(row.get("roleCode")))) {
return SecureLoginResponse.otpEnroll(challengeStore.issue(userId));
}
return SecureLoginResponse.ok(buildLoginResponse(userId, str(row.get("displayName")), hallManager));
} }
/** 2단계: 챌린지 토큰 + OTP 코드 검증 → 최종 로그인 토큰. */ /** 2단계: 챌린지 토큰 + OTP 코드 검증 → 최종 로그인 토큰. */
@ -83,42 +73,17 @@ public class TwoFactorService {
throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요."); throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요.");
} }
Map<String, Object> sec = securityMapper.findSecurityById(userId); Map<String, Object> sec = securityMapper.findSecurityById(userId);
if (sec == null || !totpService.verify(secretCipher.open(str(sec.get("otpSecret"))), code)) { if (sec == null || !totpService.verify(str(sec.get("otpSecret")), code)) {
throw new ApiException(ErrorCode.OTP_INVALID); throw new ApiException(ErrorCode.OTP_INVALID);
} }
return buildLoginResponse(userId, str(sec.get("displayName")), bool(sec.get("hallManager"))); return buildLoginResponse(userId, str(sec.get("displayName")), bool(sec.get("hallManager")));
} }
/** 로그인 중 등록 강제 — setup 단계(프리오스 챌린지 유지). 시크릿 발급·QR 반환. */ /** OTP 등록 시작 — 시크릿 발급·QR 반환(미활성, confirm 필요). */
public OtpSetupResponse enrollSetup(String challengeToken) {
String userId = challengeStore.peek(challengeToken);
if (userId == null) {
throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요.");
}
Map<String, Object> sec = securityMapper.findSecurityById(userId);
String label = sec != null ? str(sec.get("email")) : userId;
return setupOtp(userId, label != null ? label : userId);
}
/** 로그인 중 등록 강제 — verify 단계(챌린지 소비·활성화 후 최종 토큰). */
public LoginResponse enrollVerify(String challengeToken, String code) {
String userId = challengeStore.consume(challengeToken);
if (userId == null) {
throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요.");
}
Map<String, Object> sec = securityMapper.findSecurityById(userId);
if (sec == null || !totpService.verify(secretCipher.open(str(sec.get("otpSecret"))), code)) {
throw new ApiException(ErrorCode.OTP_INVALID);
}
securityMapper.enableOtp(userId);
return buildLoginResponse(userId, str(sec.get("displayName")), bool(sec.get("hallManager")));
}
/** OTP 등록 시작 — 시크릿 발급(봉인 저장)·QR 반환(미활성, confirm 필요). */
public OtpSetupResponse setupOtp(String userId, String userLabel) { public OtpSetupResponse setupOtp(String userId, String userLabel) {
String secret = totpService.generateSecret(); String secret = totpService.generateSecret();
securityMapper.saveOtpSecret(userId, secretCipher.seal(secret)); // 저장은 암호문 securityMapper.saveOtpSecret(userId, secret);
return new OtpSetupResponse(secret, // 화면 1회 표기는 평문 return new OtpSetupResponse(secret,
totpService.otpAuthUri(secret, userLabel), totpService.otpAuthUri(secret, userLabel),
totpService.qrImageDataUri(secret, userLabel)); totpService.qrImageDataUri(secret, userLabel));
} }
@ -126,30 +91,12 @@ public class TwoFactorService {
/** OTP 등록 확인 — 코드 검증 후 활성화. */ /** OTP 등록 확인 — 코드 검증 후 활성화. */
public void confirmOtp(String userId, String code) { public void confirmOtp(String userId, String code) {
Map<String, Object> sec = securityMapper.findSecurityById(userId); Map<String, Object> sec = securityMapper.findSecurityById(userId);
if (sec == null || !totpService.verify(secretCipher.open(str(sec.get("otpSecret"))), code)) { if (sec == null || !totpService.verify(str(sec.get("otpSecret")), code)) {
throw new ApiException(ErrorCode.OTP_INVALID); throw new ApiException(ErrorCode.OTP_INVALID);
} }
securityMapper.enableOtp(userId); securityMapper.enableOtp(userId);
} }
/**
* 필수 역할 판정(계약 §5B): 홀매니저·플랫폼 ADMIN/MANAGER·행사 역할 보유자(주최자/참가/장치) OTP 필수,
* 순수 관람객(행사 역할 없음) 선택. 행사 역할 유무는 event_member 판정.
*/
private boolean requiresOtp(String userId, boolean hallManager, String roleCode) {
if (hallManager) {
return true;
}
if (roleCode != null) {
String r = roleCode.toUpperCase();
if (r.equals("ADMIN") || r.equals("MANAGER")) {
return true;
}
}
List<Map<String, Object>> roleRows = userMapper.findEventRoles(userId);
return roleRows != null && !roleRows.isEmpty();
}
/** OTP 초기화(본인 해제 또는 관리자 리셋). */ /** OTP 초기화(본인 해제 또는 관리자 리셋). */
public void resetOtp(String userId) { public void resetOtp(String userId) {
securityMapper.resetOtp(userId); securityMapper.resetOtp(userId);

View File

@ -1,7 +0,0 @@
package com.zioinfo.kintex.security.dto;
import jakarta.validation.constraints.NotBlank;
/** OTP 등록 강제 흐름(로그인 중) — setup 단계 요청. 프리오스 challengeToken 만 필요. */
public record OtpEnrollRequest(@NotBlank String challengeToken) {
}

View File

@ -4,11 +4,8 @@ import com.zioinfo.kintex.auth.dto.LoginResponse;
/** /**
* 2FA 로그인 1단계 응답. status: * 2FA 로그인 1단계 응답. status:
* - "OK" OTP 미사용(관람객 ). {@code login} 최종 토큰/워크스페이스. * - "OK" OTP 미사용. {@code login} 최종 토큰/워크스페이스.
* - "OTP_REQUIRED" OTP 등록 사용자. {@code challengeToken}으로 /otp/verify 진행(2단계 코드). * - "OTP_REQUIRED" 2차 인증 필요. {@code challengeToken}으로 /otp/verify 진행.
* - "OTP_ENROLL" 필수 역할이나 OTP 미등록 등록 강제. {@code challengeToken}으로
* /otp/enroll/setup /otp/enroll/verify 진행 최종 토큰.
* challengeToken 짧은 수명(5분)·1회성 프리오스(pre-auth) 토큰(정식 JWT 아님).
*/ */
public record SecureLoginResponse(String status, String challengeToken, LoginResponse login) { public record SecureLoginResponse(String status, String challengeToken, LoginResponse login) {
@ -19,8 +16,4 @@ public record SecureLoginResponse(String status, String challengeToken, LoginRes
public static SecureLoginResponse otpRequired(String challengeToken) { public static SecureLoginResponse otpRequired(String challengeToken) {
return new SecureLoginResponse("OTP_REQUIRED", challengeToken, null); return new SecureLoginResponse("OTP_REQUIRED", challengeToken, null);
} }
public static SecureLoginResponse otpEnroll(String challengeToken) {
return new SecureLoginResponse("OTP_ENROLL", challengeToken, null);
}
} }

View File

@ -3,9 +3,6 @@
# - DB_URL / DB_USER / DB_PASSWORD : PostgreSQL(PostGIS) 접속 # - DB_URL / DB_USER / DB_PASSWORD : PostgreSQL(PostGIS) 접속
# - REDIS_HOST / REDIS_PORT : Redis 작업 큐 # - REDIS_HOST / REDIS_PORT : Redis 작업 큐
# - JWT_SECRET : JWT 서명 키(HS256, 최소 32바이트) # - JWT_SECRET : JWT 서명 키(HS256, 최소 32바이트)
# - KINTEX_OTP_ENC_KEY : OTP 시크릿 AES-256-GCM 키(base64 32바이트) — 운영 주입 필수
# - OTP_ENFORCE : 필수 역할 OTP 등록 강제(기본 true; 데모 완화 시 false)
# - ADMIN_SEED_ENABLED/ADMIN_EMAIL/ADMIN_PASSWORD : 최초 관리자 프로비저닝(하드코딩 시드 금지)
# - GEMINI_API_KEY : 나노바나나 Python 워커 전용(백엔드는 큐 발행만; 여기서 미사용) # - GEMINI_API_KEY : 나노바나나 Python 워커 전용(백엔드는 큐 발행만; 여기서 미사용)
server: server:
port: ${SERVER_PORT:8080} port: ${SERVER_PORT:8080}
@ -59,10 +56,6 @@ kintex:
lock-minutes: ${LOGIN_LOCK_MINUTES:15} lock-minutes: ${LOGIN_LOCK_MINUTES:15}
otp: otp:
issuer: ${OTP_ISSUER:KINTEX} issuer: ${OTP_ISSUER:KINTEX}
# OTP 시크릿 AES-256-GCM 암호화 키(base64 32바이트). 운영은 env 주입 필수, 미주입 시 개발 파생키.
enc-key: ${KINTEX_OTP_ENC_KEY:}
# 필수 역할(관람객 외) OTP 등록·검증 강제. 데모 편의 시 OTP_ENFORCE=false 로 완화.
enforce: ${OTP_ENFORCE:true}
# 최초 관리자 프로비저닝 — 값이 모두 주입될 때만 멱등 upsert. 미주입 시 시드 안 함(기본 관리자 계정 없음). # 최초 관리자 프로비저닝 — 값이 모두 주입될 때만 멱등 upsert. 미주입 시 시드 안 함(기본 관리자 계정 없음).
admin-seed: admin-seed:
enabled: ${ADMIN_SEED_ENABLED:false} enabled: ${ADMIN_SEED_ENABLED:false}

View File

@ -1,24 +0,0 @@
-- ============================================================================
-- V12 — 2차 인증(TOTP RFC6238) 완결: OTP 상태·로그인 실패 잠금 컬럼 멱등 정합.
-- 보안 불변(계약 §0-3): otp_secret 은 AES-256-GCM 암호화 저장(SecretCipher), API 응답/로그 노출 금지.
-- 주의: 아래 컬럼 다수는 V2/V7 에서 이미 생성됨 → 전부 IF NOT EXISTS 멱등 ALTER(재현·이관 안전).
-- ============================================================================
-- 신규: OTP 최초 활성(검증 완료) 시각 — 감사/마이페이지 상태 표시용.
ALTER TABLE app_user ADD COLUMN IF NOT EXISTS otp_verified_at timestamptz;
-- 기존 정합 재확인(멱등) — 신규 환경/부분 이관 대비.
ALTER TABLE app_user ADD COLUMN IF NOT EXISTS otp_secret varchar(255);
ALTER TABLE app_user ADD COLUMN IF NOT EXISTS otp_enabled boolean NOT NULL DEFAULT false;
ALTER TABLE app_user ADD COLUMN IF NOT EXISTS verify_method varchar(10) NOT NULL DEFAULT 'EMAIL';
ALTER TABLE app_user ADD COLUMN IF NOT EXISTS failed_login_count integer NOT NULL DEFAULT 0;
ALTER TABLE app_user ADD COLUMN IF NOT EXISTS locked_until timestamptz;
-- 암호문(enc:v1:base64) 수용을 위해 폭 확장(V2 는 varchar(96)) — 멱등.
ALTER TABLE app_user ALTER COLUMN otp_secret TYPE varchar(255);
COMMENT ON COLUMN app_user.otp_verified_at IS 'OTP 최초 활성(검증) 시각 — 감사/상태 표시';
COMMENT ON COLUMN app_user.otp_secret IS 'TOTP 시크릿(AES-256-GCM 봉인, enc:v1:) — API 응답/로그 노출 금지(계약 §0-3)';
-- 잠금 만료 조회/스윕 최적화(부분 인덱스) — 멱등.
CREATE INDEX IF NOT EXISTS idx_app_user_locked_until ON app_user (locked_until) WHERE locked_until IS NOT NULL;

View File

@ -1,12 +0,0 @@
-- V13: 카탈로그·대시보드·경영분석 집계 API 성능 인덱스 (멱등 · read-only 스키마 불변)
-- 근거: GET /api/events(카탈로그 1,098건 시작일 정렬·연도 필터) + 대시보드/BI 유틸리티 집계.
-- 신규 테이블/컬럼 없음 — 인덱스만 추가(V12 인증 트랙과 무충돌).
-- 카탈로그: 시작일 내림차순 정렬 + 연도 필터(extract(year))의 스캔 비용 완화
CREATE INDEX IF NOT EXISTS idx_event_start_date ON event (start_date DESC);
-- 카탈로그: status 원문 필터(보조)
CREATE INDEX IF NOT EXISTS idx_event_status ON event (status);
-- 대시보드/경영분석: utility_order 를 event 단위로 집계(기존 인덱스는 booth 기준만 존재)
CREATE INDEX IF NOT EXISTS idx_utility_order_event ON utility_order (event_id);

View File

@ -107,17 +107,4 @@
AND ST_Intersects(bo.geom, ST_Buffer(ex.geom, ex.clearance_m)) AND ST_Intersects(bo.geom, ST_Buffer(ex.geom, ex.clearance_m))
</select> </select>
<!-- 내부가 겹치는 부스 쌍 수: 경계만 맞닿는(ST_Touches) 경우는 제외하고 내부 교차만 계수.
ST_Overlaps는 포함/동일을 놓치므로 ST_Intersects AND NOT ST_Touches 로 포괄 판정. -->
<select id="countOverlappingBooths" resultType="int">
SELECT COUNT(*)
FROM booth a
JOIN booth b
ON a.layout_id = b.layout_id
AND a.id <![CDATA[<]]> b.id
AND ST_Intersects(a.geom, b.geom)
AND NOT ST_Touches(a.geom, b.geom)
WHERE a.layout_id = #{layoutId}
</select>
</mapper> </mapper>

View File

@ -1,6 +1,6 @@
{ {
"rulesetVersion": "compliance-v1.1", "rulesetVersion": "compliance-v1.0",
"effectiveDate": "2026-07-12", "effectiveDate": "2026-07-11",
"source": "PLANNING.md v1.2 §7-1 규정 룰셋 / 참가업체·전시주최자 매뉴얼", "source": "PLANNING.md v1.2 §7-1 규정 룰셋 / 참가업체·전시주최자 매뉴얼",
"disclaimer": "본 룰셋은 사전 필터이며 최종 승인은 킨텍스 및 구조기술사의 판단에 따른다.", "disclaimer": "본 룰셋은 사전 필터이며 최종 승인은 킨텍스 및 구조기술사의 판단에 따른다.",
"rules": [ "rules": [
@ -80,17 +80,6 @@
"operator": "eq", "operator": "eq",
"threshold": 0 "threshold": 0
}, },
{
"code": "BOOTH_OVERLAP",
"group": "layout",
"label": "부스 간 겹침 금지(배치 무결성)",
"module": ["M2"],
"severity": "block",
"metric": "layout.overlap_count",
"operator": "eq",
"threshold": 0,
"note": "PostGIS ST_Intersects AND NOT ST_Touches(내부 교차) 기반 서버 산출값"
},
{ {
"code": "CLEARANCE_WALL", "code": "CLEARANCE_WALL",
"group": "clearance", "group": "clearance",

View File

@ -1,67 +0,0 @@
package com.zioinfo.kintex.analytics;
import com.zioinfo.kintex.analytics.dto.AnalyticsDto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 경영분석 서비스 단위테스트 매출=면적×요율 산출, 단위 표기·등급, exhibitor 관점 pnl 제외.
*/
class AnalyticsServiceTest {
private AnalyticsMapper mapper;
private AnalyticsService service;
@BeforeEach
void setUp() {
mapper = mock(AnalyticsMapper.class);
service = new AnalyticsService(mapper);
}
@Test
void computesRentalRevenueFromAreaAndRate() {
when(mapper.findRentalRatePerM2()).thenReturn(2250.0);
Map<String, Object> ac = new HashMap<>();
ac.put("salesArea", 100000.0); // 100,000
ac.put("boothCount", 486);
when(mapper.findAreaAndCount("e1")).thenReturn(ac);
when(mapper.findUtilityRevenue("e1")).thenReturn(0.0);
when(mapper.findMonthlyTrend("e1", 0)).thenReturn(List.of());
AnalyticsDto dto = service.getAnalytics("e1", "operator", "event");
// 100,000 * 2250 = 225,000,000 "₩2.25억".
AnalyticsDto.Kpi rental = dto.kpis().stream()
.filter(k -> "임대 매출".equals(k.label())).findFirst().orElseThrow();
assertEquals("₩2.25억", rental.value());
// operator 관점 부스 임대 pnl 존재, grade B(1억~10억).
AnalyticsDto.PnlRow pnl = dto.pnl().stream()
.filter(p -> "부스 임대".equals(p.item())).findFirst().orElseThrow();
assertEquals(225_000_000L, pnl.amount());
assertEquals("B", pnl.grade());
}
@Test
void exhibitorPerspectiveOmitsPnl() {
when(mapper.findRentalRatePerM2()).thenReturn(2250.0);
when(mapper.findAreaAndCount("e1")).thenReturn(new HashMap<>());
when(mapper.findUtilityRevenue("e1")).thenReturn(0.0);
when(mapper.findMonthlyTrend("e1", 90)).thenReturn(List.of());
AnalyticsDto dto = service.getAnalytics("e1", "exhibitor", "d90");
assertTrue(dto.pnl().isEmpty());
assertTrue(dto.sectors().isEmpty());
assertTrue(dto.exhibitorPerf().isEmpty());
}
}

View File

@ -1,64 +0,0 @@
package com.zioinfo.kintex.common.geo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.common.error.ApiException;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 지오메트리 코덱 단위테스트 부스 폴리곤 WKT 조립( 닫힘 보정)·GeoJSON 파싱·중심 산출.
* PostGIS ST_GeomFromText 입력 정합성(SRID 0, 미터) 코드 레벨에서 보증한다.
*/
class GeometryCodecTest {
private final ObjectMapper mapper = new ObjectMapper();
@Test
void toPolygonWkt_closesOpenRing() {
// 6×3 부스(열린 ) 닫힌 POLYGON WKT
List<List<Double>> ring = List.of(
List.of(0.0, 0.0), List.of(6.0, 0.0), List.of(6.0, 3.0), List.of(0.0, 3.0));
String wkt = GeometryCodec.toPolygonWkt(ring);
assertEquals("POLYGON((0 0,6 0,6 3,0 3,0 0))", wkt);
}
@Test
void toPolygonWkt_keepsAlreadyClosedRing() {
List<List<Double>> ring = List.of(
List.of(0.0, 0.0), List.of(6.0, 0.0), List.of(6.0, 3.0),
List.of(0.0, 3.0), List.of(0.0, 0.0));
assertEquals("POLYGON((0 0,6 0,6 3,0 3,0 0))", GeometryCodec.toPolygonWkt(ring));
}
@Test
void toPolygonWkt_rejectsDegenerateRing() {
assertThrows(ApiException.class,
() -> GeometryCodec.toPolygonWkt(List.of(List.of(0.0, 0.0), List.of(1.0, 1.0))));
}
@Test
void parsePolygonRing_readsGeoJsonOuterRing() {
String geoJson = "{\"type\":\"Polygon\",\"coordinates\":[[[0,0],[6,0],[6,3],[0,3],[0,0]]]}";
List<List<Double>> ring = GeometryCodec.parsePolygonRing(mapper, geoJson);
assertEquals(5, ring.size());
assertEquals(List.of(6.0, 3.0), ring.get(2));
}
@Test
void centroid_returnsArithmeticMean() {
List<List<Double>> ring = List.of(
List.of(0.0, 0.0), List.of(6.0, 0.0), List.of(6.0, 3.0), List.of(0.0, 3.0));
assertEquals(List.of(3.0, 1.5), GeometryCodec.centroid(ring));
}
@Test
void parse_handlesNullAndBlankSafely() {
assertTrue(GeometryCodec.parsePolygonRing(mapper, null).isEmpty());
assertTrue(GeometryCodec.parseLineCoords(mapper, " ").isEmpty());
}
}

View File

@ -1,74 +0,0 @@
package com.zioinfo.kintex.dashboard;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.dashboard.dto.DashboardDto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 대시보드 서비스 단위테스트 마일스톤 파생(과거=done)·KPI 조립·미존재 행사 404.
*/
class DashboardServiceTest {
private DashboardMapper mapper;
private DashboardService service;
@BeforeEach
void setUp() {
mapper = mock(DashboardMapper.class);
service = new DashboardService(mapper);
}
@Test
void throwsNotFoundWhenEventMissing() {
when(mapper.findEventHeader("x")).thenReturn(null);
ApiException ex = assertThrows(ApiException.class, () -> service.getDashboard("x"));
assertEquals(ErrorCode.NOT_FOUND, ex.getCode());
}
@Test
void derivesMilestonesAndKpisFromRealCounts() {
// 미래 시작일 개장 upcoming, 과거 파생일(D-150 ) done.
String futureStart = LocalDate.now().plusDays(10).toString();
Map<String, Object> header = new HashMap<>();
header.put("startDate", futureStart);
when(mapper.findEventHeader("e1")).thenReturn(header);
Map<String, Object> counts = new HashMap<>();
counts.put("boothCount", 12);
counts.put("targetBooths", 20);
counts.put("designApproved", 3);
counts.put("designTotal", 8);
counts.put("utilityOrders", 5);
counts.put("renderDone", 7);
when(mapper.findKpiCounts("e1")).thenReturn(counts);
when(mapper.findExhibitors("e1")).thenReturn(List.of());
when(mapper.findRecentRenders("e1")).thenReturn(List.of());
DashboardDto dto = service.getDashboard("e1");
// 마일스톤 5종, 개장(D-0) 미래 upcoming.
assertEquals(5, dto.milestones().size());
DashboardDto.Milestone opening = dto.milestones().get(dto.milestones().size() - 1);
assertEquals("개장", opening.label());
assertEquals("upcoming", opening.state());
// 초기 D-150 파생일은 과거 done.
assertEquals("done", dto.milestones().get(0).state());
// KPI: 부스 배치 "12/20".
assertTrue(dto.kpis().stream().anyMatch(k -> "부스 배치".equals(k.label()) && "12/20".equals(k.value())));
assertTrue(dto.kpis().stream().anyMatch(k -> "설계 승인".equals(k.label()) && "3/8".equals(k.value())));
}
}

View File

@ -1,87 +0,0 @@
package com.zioinfo.kintex.module.m4;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.module.m4.dto.UtilityQuote;
import com.zioinfo.kintex.module.m4.dto.UtilityQuoteRequest;
import com.zioinfo.kintex.module.m4.mapper.UtilityOrderMapper;
import com.zioinfo.kintex.module.m4.mapper.WiringMapper;
import com.zioinfo.kintex.rules.RuleProperties;
import com.zioinfo.kintex.rules.RuleSetLoader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
/**
* M4 유틸리티 자동 견적 계산 단위테스트 실제 요율 룰셋(rates-v1.json) 기준.
* 소비전력 합산·신청용량 올림·분전반(50A/11kW) 산출·회선/급배수/압축공기 라인·합계 정합을 검증한다.
*/
class UtilityQuoteCalcTest {
private UtilityServiceImpl service;
@BeforeEach
void setUp() {
RuleProperties props = new RuleProperties();
props.setComplianceRuleset(new ClassPathResource("rulesets/compliance-v1.json"));
props.setRateRuleset(new ClassPathResource("rulesets/rates-v1.json"));
RuleSetLoader loader = new RuleSetLoader(props, new ObjectMapper());
loader.load();
service = new UtilityServiceImpl(loader, mock(WiringMapper.class),
mock(UtilityOrderMapper.class), new ObjectMapper());
}
@Test
void quote_sumsPower_boxesCeil_andLineAmounts() {
// 2대 × 5.0kW = 10kW requestedKw 10, 분전반 ceil(10/11)=1
UtilityQuoteRequest req = new UtilityQuoteRequest(
List.of(new UtilityQuoteRequest.Device("LED 사이니지", 2, 5.0)),
2, // network wired lines
1, // plumbing outlets
1); // compressed air outlets
UtilityQuote q = service.quote(req);
assertEquals(10.0, q.totalPowerKw(), 1e-9);
assertEquals(10, q.requestedKw());
assertEquals(1, q.distributionBox50A());
// rates-v1.json: perKw 55000, box50A 100000, wired 150000, plumb 150000, air 150000
long expected = 10L * 55000 // 전기
+ 1L * 100000 // 분전반
+ 2L * 150000 // 유선회선
+ 1L * 150000 // 급배수
+ 1L * 150000; // 압축공기
assertEquals(expected, q.total());
assertEquals("KRW", q.currency());
assertEquals("rates-v1.0", q.rulesetVersion());
assertEquals(5, q.lines().size());
}
@Test
void quote_fractionalKw_roundsUpRequestedCapacity() {
// 3대 × 2.2kW = 6.6kW requestedKw ceil = 7
UtilityQuoteRequest req = new UtilityQuoteRequest(
List.of(new UtilityQuoteRequest.Device("계측장비", 3, 2.2)),
0, 0, 0);
UtilityQuote q = service.quote(req);
assertEquals(7, q.requestedKw());
assertEquals(1, q.distributionBox50A()); // ceil(7/11)=1
assertEquals(7L * 55000 + 100000, q.total());
}
@Test
void quote_noDemand_isZeroWithNoLines() {
UtilityQuoteRequest req = new UtilityQuoteRequest(List.of(), 0, 0, 0);
UtilityQuote q = service.quote(req);
assertEquals(0, q.requestedKw());
assertEquals(0, q.distributionBox50A());
assertEquals(0L, q.total());
assertTrue(q.lines().isEmpty());
assertTrue(q.disclaimer() != null && !q.disclaimer().isBlank()); // 공시가 고지 항상 포함
}
}

View File

@ -1,145 +0,0 @@
package com.zioinfo.kintex.rules;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 규정 엔진 단위테스트 실제 배포 룰셋(compliance-v1.json) 로드해 평가 정확도를 검증한다.
* 대상: 높이 5m 차단·통로폭·비상구·부스 겹침·홀별 바닥하중(lteHall)·방염·리깅(warn) + submittable 집계.
*/
class ComplianceRuleEngineTest {
private ComplianceRuleEngine engine;
@BeforeEach
void setUp() {
RuleProperties props = new RuleProperties();
props.setComplianceRuleset(new ClassPathResource("rulesets/compliance-v1.json"));
props.setRateRuleset(new ClassPathResource("rulesets/rates-v1.json"));
RuleSetLoader loader = new RuleSetLoader(props, new ObjectMapper());
loader.load();
engine = new ComplianceRuleEngine(loader);
}
private Violation find(ComplianceReport r, String code) {
return r.violations().stream().filter(v -> code.equals(v.code())).findFirst().orElse(null);
}
@Test
void loadsRealRuleset() {
ComplianceReport r = engine.evaluate("M2", Map.of("booth.height_m", 3.0), null);
assertEquals("compliance-v1.1", r.rulesetVersion());
assertNotNull(r.disclaimer());
}
@Test
void m2_allWithinLimits_isSubmittable() {
Map<String, Object> m = new HashMap<>();
m.put("booth.height_m", 3.0);
m.put("layout.min_aisle_width_m", 3.5);
m.put("layout.exits_blocked_count", 0);
m.put("layout.overlap_count", 0);
m.put("booth.floor_load_t_per_m2", 4.0);
ComplianceReport r = engine.evaluate("M2", m, "H7");
assertEquals(0, r.blockCount());
assertTrue(r.submittable());
assertTrue(r.passCount() >= 5);
}
@Test
void m2_heightOverFiveMeters_blocks() {
ComplianceReport r = engine.evaluate("M2", Map.of("booth.height_m", 5.4), "H7");
Violation v = find(r, "HEIGHT_MAX");
assertNotNull(v);
assertEquals("block", v.severity());
assertFalse(r.submittable());
}
@Test
void m2_aisleNarrowerThanThreeMeters_blocks() {
ComplianceReport r = engine.evaluate("M2", Map.of("layout.min_aisle_width_m", 2.5), null);
assertNotNull(find(r, "AISLE_WIDTH_MIN"));
assertFalse(r.submittable());
}
@Test
void m2_overlappingBooths_blocks_andReportsCount() {
ComplianceReport r = engine.evaluate("M2", Map.of("layout.overlap_count", 2), null);
Violation v = find(r, "BOOTH_OVERLAP");
assertNotNull(v);
assertEquals("block", v.severity());
assertTrue(v.measured().contains("2"));
assertFalse(r.submittable());
}
@Test
void m2_floorLoad_hallSpecificLimit() {
// 홀6 상한 2.0 t/ 3.0 초과(차단).
ComplianceReport h6 = engine.evaluate("M2", Map.of("booth.floor_load_t_per_m2", 3.0), "H6");
assertNotNull(find(h6, "FLOOR_LOAD"));
// 홀7 상한 5.0 t/ 동일 3.0 통과.
ComplianceReport h7 = engine.evaluate("M2", Map.of("booth.floor_load_t_per_m2", 3.0), "H7");
assertEquals(null, find(h7, "FLOOR_LOAD"));
}
@Test
void warnViolation_doesNotBlockSubmission() {
// 인접 이격 0.1m (< 0.3m) CLEARANCE_WALL warn. warn 만이면 제출 가능(submittable true).
ComplianceReport r = engine.evaluate("M2", Map.of("booth.clearance_wall_m", 0.1), null);
Violation v = find(r, "CLEARANCE_WALL");
assertNotNull(v);
assertEquals("warn", v.severity());
assertEquals(0, r.blockCount());
assertEquals(1, r.warnCount());
assertTrue(r.submittable());
}
@Test
void riggingBetweenOperator_inRangeCurrentlyPasses() {
// 현행 룰셋: RIGGING_RANGE operator=between 6.5~8.5m 내이면 "통과" 집계(위반 미발생).
// 라벨 의도("구간 진입 시 D-7 필요") 반대 기록: _workspace/08_m2m5_contract_changes.md.
ComplianceReport r = engine.evaluate("M3", Map.of("rigging.height_m", 7.0), null);
assertEquals(null, find(r, "RIGGING_RANGE"));
assertTrue(r.passCount() >= 1);
}
@Test
void m3_nonFireRetardantMaterials_blocks() {
ComplianceReport r = engine.evaluate("M3", Map.of("materials.all_fire_retardant", false), null);
assertNotNull(find(r, "FIRE_RETARDANT"));
assertFalse(r.submittable());
}
@Test
void unmeasuredMetrics_areSkipped_notCountedAsPass() {
ComplianceReport r = engine.evaluate("M2", Map.of(), null);
assertEquals(0, r.blockCount());
assertEquals(0, r.warnCount());
assertEquals(0, r.passCount());
assertTrue(r.violations().isEmpty());
}
@Test
void unloadedRuleset_degradesGracefully() {
RuleProperties props = new RuleProperties();
props.setComplianceRuleset(new ClassPathResource("rulesets/does-not-exist.json"));
RuleSetLoader loader = new RuleSetLoader(props, new ObjectMapper());
loader.load();
ComplianceReport r = new ComplianceRuleEngine(loader)
.evaluate("M2", Map.of("booth.height_m", 9.9), null);
assertEquals("compliance-unloaded", r.rulesetVersion());
assertTrue(r.submittable());
assertEquals(List.of(), r.violations());
}
}

View File

@ -1,90 +0,0 @@
package com.zioinfo.kintex.security;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* 로그인 실패 잠금 카운터/정책 단위테스트 임계 초과 잠금, 성공 리셋, 계정 미존재 무변경.
*/
class LoginAttemptServiceTest {
private AccountSecurityMapper mapper;
private LoginAttemptService service;
@BeforeEach
void setUp() {
mapper = mock(AccountSecurityMapper.class);
SecurityPolicyProperties props = new SecurityPolicyProperties();
props.getLockout().setMaxFailedAttempts(5);
props.getLockout().setLockMinutes(30);
service = new LoginAttemptService(mapper, props);
}
private Map<String, Object> row(int failedCount, OffsetDateTime lockedUntil) {
Map<String, Object> m = new HashMap<>();
m.put("failedCount", failedCount);
m.put("lockedUntil", lockedUntil);
return m;
}
@Test
void locksAccountWhenFailuresReachThreshold() {
// 직전 4회 실패 이번 실패가 5회째 잠금.
service.recordFailure("u@kintex.test", row(4, null));
verify(mapper).incrementFailure("u@kintex.test");
ArgumentCaptor<OffsetDateTime> until = ArgumentCaptor.forClass(OffsetDateTime.class);
verify(mapper).lockUntil(org.mockito.ArgumentMatchers.eq("u@kintex.test"), until.capture());
// 잠금 만료는 미래(대략 30분 )여야 한다.
org.junit.jupiter.api.Assertions.assertTrue(until.getValue().isAfter(OffsetDateTime.now()));
}
@Test
void doesNotLockBeforeThreshold() {
service.recordFailure("u@kintex.test", row(1, null)); // 2회째
verify(mapper).incrementFailure("u@kintex.test");
verify(mapper, never()).lockUntil(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.any());
}
@Test
void missingAccountDoesNotMutateState() {
service.recordFailure("nobody@kintex.test", null);
verify(mapper, never()).incrementFailure(org.mockito.ArgumentMatchers.anyString());
verify(mapper, never()).lockUntil(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.any());
}
@Test
void assertNotLockedThrowsWhenLocked() {
Map<String, Object> locked = row(5, OffsetDateTime.now().plusMinutes(10));
ApiException ex = assertThrows(ApiException.class, () -> service.assertNotLocked(locked));
assertEquals(ErrorCode.ACCOUNT_LOCKED, ex.getCode());
}
@Test
void assertNotLockedPassesWhenLockExpired() {
Map<String, Object> expired = row(5, OffsetDateTime.now().minusMinutes(1));
assertDoesNotThrow(() -> service.assertNotLocked(expired));
}
@Test
void recordSuccessClearsLoginState() {
service.recordSuccess("user-1");
verify(mapper, times(1)).clearLoginState("user-1");
}
}

View File

@ -1,43 +0,0 @@
package com.zioinfo.kintex.security;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* OTP 시크릿 봉인/해제(AES-256-GCM) 왕복·하위호환 단위테스트.
*/
class SecretCipherTest {
private SecretCipher newCipher() {
// encKey 미주입 개발 파생키 사용(테스트 결정적).
return new SecretCipher(new SecurityPolicyProperties());
}
@Test
void sealThenOpenRoundTrips() {
SecretCipher cipher = newCipher();
String secret = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP";
String sealed = cipher.seal(secret);
assertTrue(sealed.startsWith("enc:v1:"), "봉인 접두어");
org.junit.jupiter.api.Assertions.assertNotEquals(secret, sealed, "저장값은 평문과 달라야 함");
assertEquals(secret, cipher.open(sealed), "복호화 왕복 일치");
}
@Test
void openTreatsUnprefixedAsPlaintext() {
SecretCipher cipher = newCipher();
// 이관 이전 평문 시크릿은 그대로 반환(하위호환).
assertEquals("PLAINSECRET", cipher.open("PLAINSECRET"));
}
@Test
void handlesNullAndBlank() {
SecretCipher cipher = newCipher();
assertNull(cipher.seal(null));
assertNull(cipher.open(null));
assertEquals("", cipher.seal(""));
}
}

View File

@ -1,54 +0,0 @@
package com.zioinfo.kintex.security;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* TotpService RFC6238 코드 검증 핵심 로직 단위테스트(외부 리소스 ).
*/
class TotpServiceTest {
private TotpService newService() {
SecurityPolicyProperties props = new SecurityPolicyProperties();
props.getOtp().setIssuer("KINTEX-TEST");
return new TotpService(props);
}
@Test
void generatesSecretAndVerifiesCurrentCode() throws Exception {
TotpService svc = newService();
String secret = svc.generateSecret();
assertNotNull(secret);
assertFalse(secret.isBlank());
// 현재 타임스텝의 정상 코드는 검증 통과해야 한다.
long currentBucket = new SystemTimeProvider().getTime() / 30;
String validCode = new DefaultCodeGenerator().generate(secret, currentBucket);
assertTrue(svc.verify(secret, validCode), "현재 타임스텝 코드는 유효해야 함");
}
@Test
void rejectsWrongCodeAndBlankInputs() {
TotpService svc = newService();
String secret = svc.generateSecret();
assertFalse(svc.verify(secret, "000000"), "임의 코드는 거부");
assertFalse(svc.verify(secret, ""), "빈 코드는 거부");
assertFalse(svc.verify(null, "123456"), "시크릿 null 은 거부");
assertFalse(svc.verify(secret, null), "코드 null 은 거부");
}
@Test
void otpAuthUriContainsIssuerAndSecret() {
TotpService svc = newService();
String secret = svc.generateSecret();
String uri = svc.otpAuthUri(secret, "user@kintex.test");
assertTrue(uri.startsWith("otpauth://totp/"), "otpauth URI 스킴");
assertTrue(uri.contains("KINTEX-TEST"), "issuer 포함");
assertTrue(uri.contains(secret), "secret 포함");
}
}

View File

@ -1,7 +1,6 @@
import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { useAuthStore } from './store/authStore'; import { useAuthStore } from './store/authStore';
import { LoginPage } from './screens/login/LoginPage'; import { LoginPage } from './screens/login/LoginPage';
import { OtpSetupPage } from './screens/login/OtpSetupPage';
import { BoothLayoutEditorPage } from './screens/floorplan/BoothLayoutEditorPage'; import { BoothLayoutEditorPage } from './screens/floorplan/BoothLayoutEditorPage';
import { LayoutComparisonPage } from './screens/floorplan/LayoutComparisonPage'; import { LayoutComparisonPage } from './screens/floorplan/LayoutComparisonPage';
import { OrganizerDashboardPage } from './screens/dashboard/OrganizerDashboardPage'; import { OrganizerDashboardPage } from './screens/dashboard/OrganizerDashboardPage';
@ -21,16 +20,6 @@ import { LoginSlideAdminPage } from './screens/admin/LoginSlideAdminPage';
import { ComponentGuidePage } from './screens/styleguide/ComponentGuidePage'; import { ComponentGuidePage } from './screens/styleguide/ComponentGuidePage';
import { ContractorOnsiteChecklistPage } from './screens/mobile/ContractorOnsiteChecklistPage'; import { ContractorOnsiteChecklistPage } from './screens/mobile/ContractorOnsiteChecklistPage';
import { ManagerFieldInspectionPage } from './screens/mobile/ManagerFieldInspectionPage'; import { ManagerFieldInspectionPage } from './screens/mobile/ManagerFieldInspectionPage';
import { WorklogPage } from './screens/work/WorklogPage';
import { WorkSchedulePage } from './screens/work/WorkSchedulePage';
import { MessagePage } from './screens/work/MessagePage';
import { NoticePage } from './screens/work/NoticePage';
import { OpinionPage } from './screens/work/OpinionPage';
import { SearchPage } from './screens/work/SearchPage';
import { MeetingPage } from './screens/work/MeetingPage';
import { ReportPage } from './screens/work/ReportPage';
import { NotificationCenterPage } from './screens/work/NotificationCenterPage';
import { MyPage } from './screens/work/MyPage';
import { AppShell } from './components/layout/AppShell'; import { AppShell } from './components/layout/AppShell';
/** 인증 가드 — 미인증 시 로그인으로. (역할별 라우팅은 화면 추가 시 확장) */ /** 인증 가드 — 미인증 시 로그인으로. (역할별 라우팅은 화면 추가 시 확장) */
@ -44,15 +33,6 @@ export function App() {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
{/* 2차 인증(OTP) 관리 — 인증 필요, 셸 없이 단독 카드. */}
<Route
path="/otp-setup"
element={
<RequireAuth>
<OtpSetupPage />
</RequireAuth>
}
/>
{/* 인증 필요 — 사이드바 셸 안 (데스크톱 1440) */} {/* 인증 필요 — 사이드바 셸 안 (데스크톱 1440) */}
<Route <Route
@ -117,19 +97,6 @@ export function App() {
<Route path="/admin/login-slides" element={<LoginSlideAdminPage />} /> <Route path="/admin/login-slides" element={<LoginSlideAdminPage />} />
{/* SCR-17 컴포넌트 가이드 (내부 전용 · 네비 비노출) */} {/* SCR-17 컴포넌트 가이드 (내부 전용 · 네비 비노출) */}
<Route path="/_styleguide" element={<ComponentGuidePage />} /> <Route path="/_styleguide" element={<ComponentGuidePage />} />
{/* §5B 공통 업무 기능 (SCR-39~48) — 역할 공통 영역 */}
<Route path="/work/worklog" element={<WorklogPage />} />
<Route path="/work/schedule" element={<WorkSchedulePage />} />
<Route path="/work/message" element={<MessagePage />} />
<Route path="/work/notice" element={<NoticePage />} />
<Route path="/work/opinion" element={<OpinionPage />} />
<Route path="/work/search" element={<SearchPage />} />
<Route path="/work/meeting" element={<MeetingPage />} />
<Route path="/work/report" element={<ReportPage />} />
{/* SCR-47 알림센터 · SCR-48 마이페이지 */}
<Route path="/notifications" element={<NotificationCenterPage />} />
<Route path="/me" element={<MyPage />} />
</Route> </Route>
{/* 모바일 전용 (390) — 셸 없이 전폭, 조회·현장 */} {/* 모바일 전용 (390) — 셸 없이 전폭, 조회·현장 */}

View File

@ -33,7 +33,7 @@ export function getAccessToken(): string | null {
} }
interface RequestOptions { interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown; body?: unknown;
signal?: AbortSignal; signal?: AbortSignal;
/** 인증 헤더 생략(공개 경로). */ /** 인증 헤더 생략(공개 경로). */
@ -102,8 +102,6 @@ export const api = {
request<T>(path, { ...opts, method: 'POST', body }), request<T>(path, { ...opts, method: 'POST', body }),
put: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) => put: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
request<T>(path, { ...opts, method: 'PUT', body }), request<T>(path, { ...opts, method: 'PUT', body }),
patch: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
request<T>(path, { ...opts, method: 'PATCH', body }),
del: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) => del: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
request<T>(path, { ...opts, method: 'DELETE' }), request<T>(path, { ...opts, method: 'DELETE' }),
/** multipart 업로드(POST FormData) — 파일 업로드 경로 전용. */ /** multipart 업로드(POST FormData) — 파일 업로드 경로 전용. */

View File

@ -5,19 +5,12 @@
import { api } from './client'; import { api } from './client';
import type { import type {
AcceptInviteRequest, AcceptInviteRequest,
AdminDashboardData,
AnalyticsData,
AnalyticsPeriod,
AnalyticsPerspective,
AutoLayoutOption, AutoLayoutOption,
AutoLayoutRequest, AutoLayoutRequest,
ComplianceReport, ComplianceReport,
DashboardData,
OpsData,
DesignPlanDto, DesignPlanDto,
DesignSaveRequest, DesignSaveRequest,
DesignSpec, DesignSpec,
ExhibitionDto,
ForgotPasswordRequest, ForgotPasswordRequest,
KintexPrincipal, KintexPrincipal,
LayoutDto, LayoutDto,
@ -26,67 +19,23 @@ import type {
LoginResponse, LoginResponse,
LoginSlideDto, LoginSlideDto,
LoginSlideUpdateRequest, LoginSlideUpdateRequest,
MeetingActionDto,
MeetingActionRequest,
MeetingDetail,
MeetingDto,
MeetingSaveRequest,
MessageDto,
MessageSendRequest,
NoticeDto,
NoticeSaveRequest,
NotificationDto,
OpinionCommentDto,
OpinionDetail,
OpinionDto,
OpinionSaveRequest,
OtpSetupResponse,
OtpStatusResponse,
PageResponse,
RegisterRequest, RegisterRequest,
RegisterResponse, RegisterResponse,
RenderJobDto, RenderJobDto,
RenderJobRequest, RenderJobRequest,
ReportDto,
ReportSaveRequest,
ResetPasswordRequest, ResetPasswordRequest,
ScheduleDto,
ScheduleSaveRequest,
SearchResultItem,
SecureLoginResponse,
UtilityOrderDto, UtilityOrderDto,
UtilityQuote, UtilityQuote,
UtilityQuoteRequest, UtilityQuoteRequest,
WiringRequest, WiringRequest,
WiringResult, WiringResult,
WorkStatsDto,
WorklogDto,
WorklogSaveRequest,
WorkspaceDto, WorkspaceDto,
} from './types'; } from './types';
// ── 인증·워크스페이스 (SCR-01) ── // ── 인증·워크스페이스 (SCR-01) ──
export const authApi = { export const authApi = {
// 레거시 단일 단계 로그인(하위호환 유지). 신규 UI 는 secureLogin 사용.
login: (body: LoginRequest) => login: (body: LoginRequest) =>
api.post<LoginResponse>('/api/auth/login', body, { anonymous: true }), api.post<LoginResponse>('/api/auth/login', body, { anonymous: true }),
// 2FA/잠금 적용 1단계 — status(OK|OTP_REQUIRED|OTP_ENROLL) 분기.
secureLogin: (body: LoginRequest) =>
api.post<SecureLoginResponse>('/api/auth/login/secure', body, { anonymous: true }),
// 2단계(등록 사용자) — challengeToken + 6자리 → 최종 토큰.
otpVerify: (challengeToken: string, code: string) =>
api.post<LoginResponse>('/api/auth/otp/verify', { challengeToken, code }, { anonymous: true }),
// 등록 강제 setup — challengeToken 으로 시크릿·QR 발급(프리오스).
otpEnrollSetup: (challengeToken: string) =>
api.post<OtpSetupResponse>('/api/auth/otp/enroll/setup', { challengeToken }, { anonymous: true }),
// 등록 강제 verify — 코드 확인·활성화 → 최종 토큰.
otpEnrollVerify: (challengeToken: string, code: string) =>
api.post<LoginResponse>('/api/auth/otp/enroll/verify', { challengeToken, code }, { anonymous: true }),
// 마이페이지(인증) — 본인 OTP 등록/확인/상태/해제.
otpSetup: () => api.post<OtpSetupResponse>('/api/auth/otp/setup'),
otpConfirm: (code: string) => api.post<void>('/api/auth/otp/confirm', { code }),
otpStatus: () => api.get<OtpStatusResponse>('/api/auth/otp/status'),
otpResetSelf: () => api.post<void>('/api/auth/otp/reset'),
workspaces: () => api.get<WorkspaceDto[]>('/api/auth/workspaces'), workspaces: () => api.get<WorkspaceDto[]>('/api/auth/workspaces'),
me: () => api.get<KintexPrincipal>('/api/auth/me'), me: () => api.get<KintexPrincipal>('/api/auth/me'),
acceptInvite: (body: AcceptInviteRequest) => acceptInvite: (body: AcceptInviteRequest) =>
@ -102,28 +51,6 @@ export const authApi = {
api.post<void>('/api/auth/password/reset', body, { anonymous: true }), api.post<void>('/api/auth/password/reset', body, { anonymous: true }),
}; };
// ── 전시 일정 카탈로그 (SCR-15) ──
// 제안 계약(백엔드 갭): `GET /api/events` → ExhibitionDto[] (전 홀 10년치 카탈로그).
// 인증 workspaces(내가 멤버인 행사)와 달리 이 경로는 전체 행사 카탈로그를 제공한다.
// 백엔드 미구현 시 NOT_IMPLEMENTED(501)/NOT_FOUND(404) → 호출부에서 크롤링 샘플 폴백.
export const exhibitionApi = {
list: (params?: { year?: string; status?: string }) => {
const qs = new URLSearchParams();
if (params?.year && params.year !== 'all') qs.set('year', params.year);
if (params?.status && params.status !== 'all') qs.set('status', params.status);
const q = qs.toString();
return api.get<ExhibitionDto[]>(`/api/events${q ? `?${q}` : ''}`);
},
};
// ── 시스템관리 — 사용자(관리자) ──
// 사용자 관리 화면은 후속(현재 API 계약만). OTP 초기화는 SystemAccessGuard.requireAdmin + @Audited.
export const adminUserApi = {
// 관리자 — 대상 사용자 OTP 강제 초기화(분실/기기 교체 대응). 다음 로그인 시 재등록.
otpReset: (userId: string) =>
api.post<void>(`/api/admin/users/${encodeURIComponent(userId)}/otp/reset`),
};
// ── 로그인 슬라이드 (SCR-01 캐러셀 · 관리자) ── // ── 로그인 슬라이드 (SCR-01 캐러셀 · 관리자) ──
export const loginSlideApi = { export const loginSlideApi = {
// 공개 — 활성만, 최신 등록순 최대 5건(인증 불필요). // 공개 — 활성만, 최신 등록순 최대 5건(인증 불필요).
@ -219,165 +146,3 @@ export const renderApi = {
body, body,
), ),
}; };
/*
* (SCR-02·13·14·16) API
* 근거: 백엔드 com.zioinfo.kintex.{dashboard,analytics,admin,ops} ().
* (NETWORK/NOT_FOUND) .
* */
// ── SCR-02 주최자 대시보드 집계 ──
export const dashboardApi = {
get: (eventId: string) =>
api.get<DashboardData>(`/api/events/${encodeURIComponent(eventId)}/dashboard`),
};
// ── SCR-13 경영분석 BI 집계 ──
export const analyticsApi = {
get: (eventId: string, perspective: AnalyticsPerspective, period: AnalyticsPeriod) =>
api.get<AnalyticsData>(
`/api/events/${encodeURIComponent(eventId)}/analytics?perspective=${perspective}&period=${period}`,
),
};
// ── SCR-14 관리자 백오피스 대시보드 ──
export const adminApi = {
dashboard: (tenant?: string) =>
api.get<AdminDashboardData>(
`/api/admin/dashboard${tenant ? `?tenant=${encodeURIComponent(tenant)}` : ''}`,
),
};
// ── SCR-16 홀 현장 운영 ──
export const opsApi = {
get: (eventId: string) =>
api.get<OpsData>(`/api/events/${encodeURIComponent(eventId)}/ops`),
};
/*
* §5B (SCR-39~48) /api/work/*
* 근거: 백엔드 com.zioinfo.kintex.work.* ().
* */
function qs(params: Record<string, string | number | boolean | undefined | null>): string {
const sp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== '') sp.set(k, String(v));
}
const s = sp.toString();
return s ? `?${s}` : '';
}
// ── SCR-39 업무일지 ──
export interface WorklogListParams {
eventId?: string;
fromDate?: string;
toDate?: string;
status?: string;
page?: number;
size?: number;
}
export const worklogApi = {
list: (p: WorklogListParams = {}) =>
api.get<PageResponse<WorklogDto>>(`/api/work/worklogs${qs({ ...p })}`),
get: (id: string) => api.get<WorklogDto>(`/api/work/worklogs/${encodeURIComponent(id)}`),
create: (body: WorklogSaveRequest) => api.post<WorklogDto>('/api/work/worklogs', body),
update: (id: string, body: WorklogSaveRequest) =>
api.put<WorklogDto>(`/api/work/worklogs/${encodeURIComponent(id)}`, body),
remove: (id: string) => api.del<void>(`/api/work/worklogs/${encodeURIComponent(id)}`),
};
// ── SCR-40 통합 일정 ──
export const scheduleWorkApi = {
list: (p: { eventId?: string; fromAt?: string; toAt?: string } = {}) =>
api.get<ScheduleDto[]>(`/api/work/schedules${qs({ ...p })}`),
get: (id: string) => api.get<ScheduleDto>(`/api/work/schedules/${encodeURIComponent(id)}`),
create: (body: ScheduleSaveRequest) => api.post<ScheduleDto>('/api/work/schedules', body),
update: (id: string, body: ScheduleSaveRequest) =>
api.put<ScheduleDto>(`/api/work/schedules/${encodeURIComponent(id)}`, body),
remove: (id: string) => api.del<void>(`/api/work/schedules/${encodeURIComponent(id)}`),
};
// ── SCR-41 쪽지 ──
export const messageApi = {
inbox: (page = 0, size = 20) =>
api.get<PageResponse<MessageDto>>(`/api/work/messages/inbox${qs({ page, size })}`),
sent: (page = 0, size = 20) =>
api.get<PageResponse<MessageDto>>(`/api/work/messages/sent${qs({ page, size })}`),
unreadCount: () => api.get<{ unread: number }>('/api/work/messages/unread-count'),
send: (body: MessageSendRequest) => api.post<{ id: string }>('/api/work/messages', body),
read: (id: string) => api.post<void>(`/api/work/messages/${encodeURIComponent(id)}/read`),
remove: (id: string) => api.del<void>(`/api/work/messages/${encodeURIComponent(id)}`),
};
// ── SCR-42 공지 (조회=인증, 쓰기=매니저) ──
export const noticeApi = {
list: (p: { eventId?: string; category?: string; keyword?: string; page?: number; size?: number } = {}) =>
api.get<PageResponse<NoticeDto>>(`/api/work/notices${qs({ ...p })}`),
get: (id: string) => api.get<NoticeDto>(`/api/work/notices/${encodeURIComponent(id)}`),
create: (body: NoticeSaveRequest) => api.post<NoticeDto>('/api/work/notices', body),
update: (id: string, body: NoticeSaveRequest) =>
api.put<NoticeDto>(`/api/work/notices/${encodeURIComponent(id)}`, body),
remove: (id: string) => api.del<void>(`/api/work/notices/${encodeURIComponent(id)}`),
};
// ── SCR-43 의견접수 (조회/등록=인증, 상태·답변=매니저) ──
export const opinionApi = {
list: (p: { eventId?: string; status?: string; page?: number; size?: number } = {}) =>
api.get<PageResponse<OpinionDto>>(`/api/work/opinions${qs({ ...p })}`),
get: (id: string) => api.get<OpinionDetail>(`/api/work/opinions/${encodeURIComponent(id)}`),
create: (body: OpinionSaveRequest) => api.post<OpinionDto>('/api/work/opinions', body),
changeStatus: (id: string, value: string) =>
api.patch<void>(`/api/work/opinions/${encodeURIComponent(id)}/status${qs({ value })}`),
comment: (id: string, content: string) =>
api.post<OpinionCommentDto>(`/api/work/opinions/${encodeURIComponent(id)}/comments`, { content }),
remove: (id: string) => api.del<void>(`/api/work/opinions/${encodeURIComponent(id)}`),
};
// ── SCR-44 통합검색 ──
export const searchApi = {
search: (q: string, limit = 30) =>
api.get<SearchResultItem[]>(`/api/work/search${qs({ q, limit })}`),
};
// ── SCR-45 회의록 ──
export const meetingApi = {
list: (p: { eventId?: string; page?: number; size?: number } = {}) =>
api.get<PageResponse<MeetingDto>>(`/api/work/meetings${qs({ ...p })}`),
get: (id: string) => api.get<MeetingDetail>(`/api/work/meetings/${encodeURIComponent(id)}`),
create: (body: MeetingSaveRequest) => api.post<MeetingDto>('/api/work/meetings', body),
update: (id: string, body: MeetingSaveRequest) =>
api.put<MeetingDto>(`/api/work/meetings/${encodeURIComponent(id)}`, body),
saveMinutes: (id: string, minutes: string) =>
api.put<void>(`/api/work/meetings/${encodeURIComponent(id)}/minutes`, { minutes }),
addAction: (id: string, body: MeetingActionRequest) =>
api.post<MeetingActionDto>(`/api/work/meetings/${encodeURIComponent(id)}/actions`, body),
actionStatus: (actionId: string, value: string) =>
api.patch<void>(`/api/work/meetings/actions/${encodeURIComponent(actionId)}/status${qs({ value })}`),
remove: (id: string) => api.del<void>(`/api/work/meetings/${encodeURIComponent(id)}`),
};
// ── SCR-46 업무보고 + 통계 ──
export const reportApi = {
list: (p: { eventId?: string; reportType?: string; page?: number; size?: number } = {}) =>
api.get<PageResponse<ReportDto>>(`/api/work/reports${qs({ ...p })}`),
get: (id: string) => api.get<ReportDto>(`/api/work/reports/${encodeURIComponent(id)}`),
create: (body: ReportSaveRequest) => api.post<ReportDto>('/api/work/reports', body),
update: (id: string, body: ReportSaveRequest) =>
api.put<ReportDto>(`/api/work/reports/${encodeURIComponent(id)}`, body),
remove: (id: string) => api.del<void>(`/api/work/reports/${encodeURIComponent(id)}`),
};
export const workStatsApi = {
worklog: (p: { eventId?: string; fromDate?: string; toDate?: string } = {}) =>
api.get<WorkStatsDto>(`/api/work/stats/worklog${qs({ ...p })}`),
};
// ── SCR-47 알림센터 ──
export const notificationApi = {
list: (p: { unreadOnly?: boolean; page?: number; size?: number } = {}) =>
api.get<PageResponse<NotificationDto>>(`/api/work/notifications${qs({ ...p })}`),
unreadCount: () => api.get<{ unread: number }>('/api/work/notifications/unread-count'),
read: (id: string) => api.post<void>(`/api/work/notifications/${encodeURIComponent(id)}/read`),
readAll: () => api.post<void>('/api/work/notifications/read-all'),
remove: (id: string) => api.del<void>(`/api/work/notifications/${encodeURIComponent(id)}`),
};

View File

@ -49,22 +49,6 @@ export interface WorkspaceDto {
myRole: EventRole; myRole: EventRole;
dday: number; dday: number;
} }
// ── 전시 일정 카탈로그 (SCR-15 · M1·M6) ──
// 원천: `event` 테이블(V10 seed_exhibitions_10yr — kintex.com 크롤링 10년치 1,098건).
// 제안 계약: `GET /api/events` → ExhibitionDto[] (백엔드 갭 06_backend_api_gaps.md §1).
// DB 컬럼(id·name·start_date·end_date·status)은 필수, 나머지는 후속 enrich(nullable).
export type ExhibitionStatus = 'ongoing' | 'upcoming' | 'ended';
export interface ExhibitionDto {
id: string;
name: string;
startDate: string; // "2026-08-11"
endDate: string; // "2026-08-14"
status: ExhibitionStatus | string; // DB 원문(ended 등) — 프론트는 날짜 기준 재계산
hallLabel?: string | null; // 배정 홀(후속 enrich) — 예 "제2전시장 홀7"
category?: string | null; // 카테고리(후속 enrich)
estVisitors?: number | null; // 추정 인원(후속 enrich)
}
export interface AuthUser { export interface AuthUser {
userId: string; userId: string;
displayName: string; displayName: string;
@ -80,28 +64,6 @@ export interface LoginRequest {
email: string; email: string;
password: string; password: string;
} }
// ── 2차 인증(OTP·TOTP) — 로그인 2단계 + 등록 강제 ──
// 1단계 /api/auth/login/secure 응답 status:
// - OK → OTP 미사용(관람객). login 에 최종 토큰.
// - OTP_REQUIRED → 등록 사용자. challengeToken 으로 /otp/verify(6자리) → 최종 토큰.
// - OTP_ENROLL → 필수 역할 미등록. challengeToken 으로 /otp/enroll/setup → /otp/enroll/verify.
export type SecureLoginStatus = 'OK' | 'OTP_REQUIRED' | 'OTP_ENROLL';
export interface SecureLoginResponse {
status: SecureLoginStatus;
challengeToken: string | null;
login: LoginResponse | null;
}
// 시크릿·otpauth URI·QR(data:image/png;base64) — 등록 화면 1회 표기용(시크릿은 저장/재노출 금지).
export interface OtpSetupResponse {
secret: string;
otpAuthUri: string;
qrImageDataUri: string;
}
export interface OtpStatusResponse {
otpEnabled: boolean;
verifyMethod: string;
}
export interface AcceptInviteRequest { export interface AcceptInviteRequest {
inviteCode: string; inviteCode: string;
companyRegistrationNo: string; companyRegistrationNo: string;
@ -360,360 +322,3 @@ export interface LoginSlideUpdateRequest {
sortOrder?: number | null; sortOrder?: number | null;
active?: boolean; active?: boolean;
} }
/*
* §5B (SCR-39~48) com.zioinfo.kintex.work.*
* DTO는 / ( ).
* */
// ── SCR-39 업무일지 (work/worklog) ──
export interface WorklogDto {
id: string;
eventId: string | null;
writerId: string;
writerName: string;
workDate: string; // YYYY-MM-DD
title: string;
workType: string | null;
progress: string | null;
status: string | null;
content: string | null;
hours: number;
createdAt: string;
updatedAt: string;
}
export interface WorklogSaveRequest {
eventId?: string | null;
workDate: string;
title: string;
workType?: string | null;
progress?: string | null;
status?: string | null;
content?: string | null;
hours?: number | null;
}
// ── SCR-40 통합 일정 (work/schedule) ──
export interface ScheduleDto {
id: string;
eventId: string | null;
ownerId: string;
ownerName: string;
title: string;
scheduleType: string | null;
importance: string | null;
startAt: string; // ISO-8601
endAt: string | null;
allDay: boolean;
location: string | null;
content: string | null;
color: string | null;
createdAt: string;
}
export interface ScheduleSaveRequest {
eventId?: string | null;
title: string;
scheduleType?: string | null;
importance?: string | null;
startAt: string;
endAt?: string | null;
allDay?: boolean | null;
location?: string | null;
content?: string | null;
color?: string | null;
}
// ── SCR-41 쪽지 (work/message) ──
export interface MessageDto {
id: string;
senderId: string;
senderName: string;
title: string;
content: string;
recvType: string | null; // RECV | REF (수신함 뷰)
readAt: string | null;
createdAt: string;
}
export interface MessageSendRequest {
title?: string | null;
content?: string | null;
recipientIds: string[]; // RECV
refIds?: string[]; // REF
}
// ── SCR-42 공지 (work/notice) ──
export interface NoticeDto {
id: string;
eventId: string | null;
category: string | null;
title: string;
content: string | null;
pinned: boolean;
authorId: string;
authorName: string;
viewCount: number;
publishedAt: string | null;
createdAt: string;
}
export interface NoticeSaveRequest {
eventId?: string | null;
category?: string | null;
title: string;
content?: string | null;
pinned?: boolean | null;
}
// ── SCR-43 의견접수 (work/opinion) ──
export interface OpinionDto {
id: string;
eventId: string | null;
category: string | null;
title: string;
content: string | null;
status: string; // 접수 RECEIVED | 검토중 REVIEWING | 답변완료 ANSWERED (백엔드 권위)
authorId: string;
authorName: string;
secretYn: string | null;
createdAt: string;
}
export interface OpinionCommentDto {
id: string;
opinionId: string;
authorId: string;
authorName: string;
content: string;
createdAt: string;
}
export interface OpinionDetail {
opinion: OpinionDto;
comments: OpinionCommentDto[];
}
export interface OpinionSaveRequest {
eventId?: string | null;
category?: string | null;
title: string;
content?: string | null;
secretYn?: string | null;
}
// ── SCR-44 통합검색 (work/search) ──
// type: WORKLOG | NOTICE | MEETING | REPORT | OPINION
export type SearchResultType = 'WORKLOG' | 'NOTICE' | 'MEETING' | 'REPORT' | 'OPINION' | string;
export interface SearchResultItem {
type: SearchResultType;
id: string;
title: string;
createdAt: string;
}
// ── SCR-45 회의록 (work/meeting) ──
export interface MeetingDto {
id: string;
eventId: string | null;
title: string;
location: string | null;
meetingAt: string; // ISO-8601
organizerId: string;
organizerName: string;
content: string | null;
minutes: string | null;
createdAt: string;
}
export interface MeetingActionDto {
id: string;
meetingId: string;
seq: number;
actionItem: string;
assigneeId: string | null;
dueDate: string | null;
status: string; // OPEN | DONE …
}
export interface MeetingDetail {
meeting: MeetingDto;
actions: MeetingActionDto[];
}
export interface MeetingSaveRequest {
eventId?: string | null;
title: string;
location?: string | null;
meetingAt: string;
content?: string | null;
}
export interface MeetingActionRequest {
actionItem: string;
assigneeId?: string | null;
dueDate?: string | null;
}
// ── SCR-46 업무보고 (work/report) + 통계 (work/stats) ──
export type ReportType = 'DAILY' | 'WEEKLY' | 'MONTHLY' | string;
export interface ReportDto {
id: string;
eventId: string | null;
reportType: string;
periodFrom: string | null;
periodTo: string | null;
title: string;
content: string | null;
authorId: string;
authorName: string;
createdAt: string;
}
export interface ReportSaveRequest {
eventId?: string | null;
reportType?: string | null;
periodFrom?: string | null;
periodTo?: string | null;
title: string;
content?: string | null;
}
export interface StatItem {
name: string;
count: number;
hours: number;
}
export interface WorkStatsDto {
totalCount: number;
totalHours: number;
byStatus: StatItem[];
byType: StatItem[];
byWriter: StatItem[];
}
// ── SCR-47 알림센터 (work/notification) ──
export interface NotificationDto {
id: string;
recipientId: string;
eventId: string | null;
notiType: string;
title: string;
message: string | null;
link: string | null;
readAt: string | null;
createdAt: string;
}
/*
* (SCR-02·13·14·16)
* 정본: 백엔드 record com.zioinfo.kintex.{dashboard,analytics,admin,ops}.dto.*
* data. 0/null/ .
* */
// ── SCR-02 주최자 대시보드 (dashboard.DashboardDto) ──
export interface DashboardMilestone {
label: string;
date: string; // "YYYY-MM-DD"
state: 'done' | 'current' | 'upcoming' | string;
}
export interface DashboardKpi {
label: string;
value: string;
delta: string | null;
trend: 'up' | 'down' | 'flat' | string;
}
export interface DashboardExhibitor {
companyName: string;
boothNo: string;
status: string | null;
}
export interface DashboardFeed {
id: string;
kind: string; // "render" 등
message: string;
at: string; // ISO
}
export interface DashboardData {
milestones: DashboardMilestone[];
kpis: DashboardKpi[];
exhibitors: DashboardExhibitor[];
feed: DashboardFeed[];
}
// ── SCR-13 경영분석 BI (analytics.AnalyticsDto) ──
export type AnalyticsPerspective = 'operator' | 'exhibitor';
export type AnalyticsPeriod = 'event' | 'd90' | 'annual';
export interface AnalyticsKpi {
label: string;
value: string;
delta: string | null;
trend: 'up' | 'down' | 'flat' | string;
}
export interface AnalyticsTrendPoint {
period: string; // "YYYY-MM"
revenue: number;
booths: number;
}
export interface AnalyticsSector {
name: string;
sharePercent: number;
revenue: number;
}
export interface AnalyticsPnlRow {
item: string;
amount: number;
grade: 'A' | 'B' | 'C' | string;
}
export interface AnalyticsExhibitorPerf {
companyName: string;
leads: number;
conversionPercent: number;
}
export interface AnalyticsData {
kpis: AnalyticsKpi[];
trend: AnalyticsTrendPoint[];
sectors: AnalyticsSector[];
pnl: AnalyticsPnlRow[];
exhibitorPerf: AnalyticsExhibitorPerf[];
}
// ── SCR-14 관리자 백오피스 (admin.AdminDashboardDto) ──
export interface AdminKpiDto {
label: string;
value: string;
sub: string | null;
}
export interface AdminVisitorPoint {
hour: string;
count: number;
}
export interface AdminLiveEvent {
eventId: string;
name: string;
hall: string | null;
occupancy: number;
status: string;
}
export interface AdminDashboardData {
kpis: AdminKpiDto[];
visitorTrend: AdminVisitorPoint[];
liveEvents: AdminLiveEvent[];
tenants: string[];
}
// ── SCR-16 홀 현장 운영 (ops.OpsDto) ──
export interface OpsHall {
hallId: string;
label: string;
estPeople: number;
heat: 'smooth' | 'moderate' | 'busy' | string;
}
export interface OpsHvac {
tempC: number | null;
humidityPercent: number | null;
co2Ppm: number | null;
}
export interface OpsLightingZone {
zone: string;
onPercent: number;
}
export interface OpsParkingZone {
zone: string;
occupancy: number;
capacity: number;
}
export interface OpsData {
halls: OpsHall[];
hvac: OpsHvac;
lighting: OpsLightingZone[];
parking: OpsParkingZone[];
}

View File

@ -1,21 +1,18 @@
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { NavLink, Outlet, useNavigate } from 'react-router-dom'; import { NavLink, Outlet } from 'react-router-dom';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { DdayChip } from '../ui/Badge'; import { DdayChip } from '../ui/Badge';
import { import {
IconAnalytics, IconAnalytics,
IconBell, IconBell,
IconCalendar, IconCalendar,
IconCheckCircle,
IconDashboard, IconDashboard,
IconDocument, IconDocument,
IconExhibitors, IconExhibitors,
IconFloorplan, IconFloorplan,
IconOperations, IconOperations,
IconSearch,
IconSettings, IconSettings,
IconSettlement, IconSettlement,
IconUsers,
type IconProps, type IconProps,
} from '../ui/icons'; } from '../ui/icons';
import './app-shell.css'; import './app-shell.css';
@ -37,18 +34,6 @@ const NAV_ITEMS: { key: string; label: string; Icon: ComponentType<IconProps>; t
{ key: 'admin', label: '관리자', Icon: IconSettings, to: '/admin' }, { key: 'admin', label: '관리자', Icon: IconSettings, to: '/admin' },
]; ];
/** §5B 공통 업무 기능(SCR-39~48) — 전 역할 공통 영역. */
const WORK_NAV: { key: string; label: string; Icon: ComponentType<IconProps>; to: string }[] = [
{ key: 'worklog', label: '업무일지', Icon: IconDocument, to: '/work/worklog' },
{ key: 'work-schedule', label: '일정', Icon: IconCalendar, to: '/work/schedule' },
{ key: 'message', label: '쪽지', Icon: IconUsers, to: '/work/message' },
{ key: 'notice', label: '공지', Icon: IconBell, to: '/work/notice' },
{ key: 'meeting', label: '회의록', Icon: IconOperations, to: '/work/meeting' },
{ key: 'report', label: '업무보고', Icon: IconAnalytics, to: '/work/report' },
{ key: 'opinion', label: '의견접수', Icon: IconCheckCircle, to: '/work/opinion' },
{ key: 'search', label: '통합검색', Icon: IconSearch, to: '/work/search' },
];
/** /**
* (design.md §2-2) 240px + (·D-·). * (design.md §2-2) 240px + (·D-·).
* . * .
@ -57,7 +42,6 @@ export function AppShell() {
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const workspace = useAuthStore((s) => s.currentWorkspace()); const workspace = useAuthStore((s) => s.currentWorkspace());
const logout = useAuthStore((s) => s.logout); const logout = useAuthStore((s) => s.logout);
const navigate = useNavigate();
return ( return (
<div className="kx-shell"> <div className="kx-shell">
@ -100,37 +84,11 @@ export function AppShell() {
))} ))}
</ul> </ul>
<div className="kx-shell__nav-label"> </div>
<ul className="kx-shell__nav">
{WORK_NAV.map((item) => (
<li key={item.key}>
<NavLink
to={item.to}
className={({ isActive }) => `kx-shell__nav-link ${isActive ? 'is-active' : ''}`}
>
<span className="kx-shell__nav-icon"><item.Icon size={20} /></span>
{item.label}
</NavLink>
</li>
))}
</ul>
<div className="kx-shell__user"> <div className="kx-shell__user">
<button <span className="kx-shell__avatar" aria-hidden="true">
className="kx-shell__avatar kx-shell__avatar--btn"
type="button"
aria-label="마이페이지"
onClick={() => navigate('/me')}
>
{user?.displayName?.[0] ?? '·'} {user?.displayName?.[0] ?? '·'}
</button> </span>
<button <span className="kx-shell__user-name">{user?.displayName ?? '사용자'}</span>
className="kx-shell__user-name kx-shell__user-name--btn"
type="button"
onClick={() => navigate('/me')}
>
{user?.displayName ?? '사용자'}
</button>
<button className="kx-shell__logout" type="button" onClick={logout}> <button className="kx-shell__logout" type="button" onClick={logout}>
</button> </button>
@ -146,12 +104,7 @@ export function AppShell() {
</div> </div>
<div className="kx-shell__topbar-right"> <div className="kx-shell__topbar-right">
{workspace && <DdayChip dday={workspace.dday} />} {workspace && <DdayChip dday={workspace.dday} />}
<button <button className="kx-shell__icon-btn" type="button" aria-label="알림">
className="kx-shell__icon-btn"
type="button"
aria-label="알림"
onClick={() => navigate('/notifications')}
>
<IconBell size={20} /> <IconBell size={20} />
</button> </button>
</div> </div>

View File

@ -138,39 +138,6 @@
color: var(--color-primary-600); color: var(--color-primary-600);
} }
/* §5B 업무 공통 2번째 내비 그룹 — 콘텐츠 높이만 차지, 사이드바 스크롤. */
.kx-shell__sidebar {
overflow-y: auto;
}
.kx-shell__nav {
flex: 0 0 auto;
}
.kx-shell__nav-label {
padding: var(--space-4) var(--space-5) var(--space-2);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--color-neutral-500);
text-transform: uppercase;
}
.kx-shell__user {
margin-top: auto;
}
.kx-shell__avatar--btn {
border: none;
cursor: pointer;
}
.kx-shell__user-name--btn {
background: none;
border: none;
text-align: left;
cursor: pointer;
padding: 0;
}
.kx-shell__user-name--btn:hover {
color: var(--color-primary-600);
}
.kx-shell__main { .kx-shell__main {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@ -1,14 +0,0 @@
import { useParams } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
/**
* 라우트에 :eventId (SCR-13 /analytics · SCR-16 /ops) .
* 우선순위: 라우트 param currentEventId .
* (LoginPage selectEvent(ws.eventId) currentEventId .)
*/
export function useResolvedEventId(): string | null {
const { eventId: routeEventId } = useParams();
const currentEventId = useAuthStore((s) => s.currentEventId);
const firstWorkspace = useAuthStore((s) => s.workspaces[0]?.eventId ?? null);
return routeEventId ?? currentEventId ?? firstWorkspace;
}

View File

@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { import {
Bar, Bar,
BarChart, BarChart,
@ -11,33 +10,26 @@ import {
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import { CHART } from '../chartColors'; import { CHART } from '../chartColors';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States'; import {
import { adminApi } from '../../api/endpoints'; ADMIN_KPIS,
import { ApiRequestError } from '../../api/client'; LIVE_EVENTS,
import { StatusPill } from '../work/workShared'; TENANTS,
import type { AdminDashboardData } from '../../api/types'; VISITOR_TREND,
import { ADMIN_KPIS, LIVE_EVENTS, TENANTS, VISITOR_TREND } from './sampleAdmin'; } from './sampleAdmin';
import './admin.css'; import './admin.css';
const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
/* /*
* SCR-14 (M18 ). Stitch admin_dashboard . * SCR-16 (M18 ). Stitch admin_dashboard .
* 경로: GET /api/admin/dashboard?tenant ( / ·· ). * - 개요: KPI 3 + (/) + .
* 폴백: NETWORK/NOT_FOUND sampleAdmin . . * (): (/ ). AI P2 ().
* M18 (···) (/admin/*).
*/ */
export function AdminDashboardPage() { export function AdminDashboardPage() {
const [tenant, setTenant] = useState<string | undefined>(undefined); const [range, setRange] = useState<'today' | 'weekly'>('today');
const [tenant, setTenant] = useState(TENANTS[0]);
const q = useQuery({
queryKey: ['admin-dashboard', tenant],
queryFn: () => adminApi.dashboard(tenant),
retry: false,
});
const degraded = isDegradable(q.error);
const data: AdminDashboardData | null = q.data ?? (degraded ? FALLBACK : null);
const hardError = q.isError && !degraded;
const tenants = data?.tenants?.length ? data.tenants : TENANTS;
const activeTenant = tenant ?? tenants[0];
return ( return (
<div className="kx-admin"> <div className="kx-admin">
@ -47,16 +39,18 @@ export function AdminDashboardPage() {
<p className="kx-admin__subtitle"> </p> <p className="kx-admin__subtitle"> </p>
</div> </div>
<div className="kx-admin__head-actions"> <div className="kx-admin__head-actions">
{/* 관리자 하위 화면 — 로그인 슬라이드 관리 */}
<Link to="/admin/login-slides" className="kx-btn kx-btn--secondary"> <Link to="/admin/login-slides" className="kx-btn kx-btn--secondary">
</Link> </Link>
{/* 플랫폼 슈퍼관리자 전용 테넌트 스위처(샘플) */}
<select <select
className="kx-select" className="kx-select"
aria-label="테넌트 전환" aria-label="테넌트 전환"
value={activeTenant} value={tenant}
onChange={(e) => setTenant(e.target.value)} onChange={(e) => setTenant(e.target.value)}
> >
{tenants.map((t) => ( {TENANTS.map((t) => (
<option key={t} value={t}>{t}</option> <option key={t} value={t}>{t}</option>
))} ))}
</select> </select>
@ -66,57 +60,49 @@ export function AdminDashboardPage() {
</div> </div>
</header> </header>
{degraded && (
<div className="kx-admin__degraded-bar"> <div className="kx-admin__degraded-bar">
<span className="kx-bi__degraded"> </span> <span className="kx-bi__degraded"> API ()</span>
API에 . ·· . (M18) .
</div> </div>
)}
{q.isLoading && <AdminSkeleton />}
{hardError && (
<ErrorState message="관리자 대시보드를 불러오지 못했습니다." onRetry={() => q.refetch()} />
)}
{data && (
<>
<section className="kx-admin__kpis" aria-label="핵심 지표"> <section className="kx-admin__kpis" aria-label="핵심 지표">
{data.kpis.length === 0 ? ( {ADMIN_KPIS.map((k) => (
<EmptyState title="집계된 지표가 없습니다" /> <div key={k.key} className={`kx-kpi kx-kpi--${k.tone ?? 'normal'}`}>
) : (
data.kpis.map((k, i) => (
<div key={`${k.label}-${i}`} className="kx-kpi kx-kpi--normal">
<span className="kx-kpi__label">{k.label}</span> <span className="kx-kpi__label">{k.label}</span>
<strong className="kx-kpi__value tnum">{k.value}</strong> <strong className="kx-kpi__value tnum">{k.value}</strong>
{k.sub && <span className="kx-kpi__sub">{k.sub}</span>} {k.delta && (
</div> <span className={`kx-delta kx-delta--${k.deltaDir ?? 'up'}`}>
)) {k.tone === 'warn' ? '⚠' : k.deltaDir === 'down' ? '▼' : '▲'} {k.delta}
</span>
)} )}
{k.bar != null && (
<div className="kx-barlist__track" style={{ marginTop: 8 }}>
<span className="kx-barlist__fill is-error" style={{ width: `${k.bar}%` }} />
</div>
)}
</div>
))}
</section> </section>
<div className="kx-admin__split"> <div className="kx-admin__split">
<section className="kx-card kx-admin__chart-card" aria-label="방문객 추이"> <section className="kx-card kx-admin__chart-card" aria-label="방문객 추이">
<div className="kx-card__head"> <div className="kx-card__head">
<h2> </h2> <h2> ({range === 'today' ? '오늘' : '주간 평균'})</h2>
<div className="kx-seg" role="tablist" aria-label="추이 기간">
<button role="tab" aria-selected={range === 'today'} className={`kx-seg__btn ${range === 'today' ? 'is-active' : ''}`} onClick={() => setRange('today')}></button>
<button role="tab" aria-selected={range === 'weekly'} className={`kx-seg__btn ${range === 'weekly' ? 'is-active' : ''}`} onClick={() => setRange('weekly')}></button>
</div>
</div> </div>
<div className="kx-admin__chart"> <div className="kx-admin__chart">
{data.visitorTrend.length === 0 ? (
<EmptyState
title="관람객 추이 미집계"
description="관람객 계측 센서(M14)가 연동되면 시간대별 추이가 표시됩니다."
/>
) : (
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<BarChart data={data.visitorTrend} margin={{ top: 8, right: 8, bottom: 0, left: -8 }}> <BarChart data={VISITOR_TREND} margin={{ top: 8, right: 8, bottom: 0, left: -8 }}>
<CartesianGrid stroke={CHART.neutral200} strokeDasharray="3 3" vertical={false} /> <CartesianGrid stroke={CHART.neutral200} strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="hour" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={{ stroke: CHART.neutral200 }} /> <XAxis dataKey="hour" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={{ stroke: CHART.neutral200 }} />
<YAxis tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={44} /> <YAxis tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={44} />
<Tooltip cursor={{ fill: CHART.primary050 }} contentStyle={TOOLTIP_STYLE} /> <Tooltip cursor={{ fill: CHART.primary050 }} contentStyle={TOOLTIP_STYLE} />
<Bar dataKey="count" name="관람객" fill={CHART.primary600} radius={[4, 4, 0, 0]} maxBarSize={34} /> <Bar dataKey={range} name="방문객" fill={CHART.primary600} radius={[4, 4, 0, 0]} maxBarSize={34} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
)}
</div> </div>
</section> </section>
@ -124,34 +110,29 @@ export function AdminDashboardPage() {
<div className="kx-card__head"> <div className="kx-card__head">
<h2> </h2> <h2> </h2>
</div> </div>
{data.liveEvents.length === 0 ? (
<EmptyState title="진행 중인 행사가 없습니다" />
) : (
<div className="kx-admin__live-list"> <div className="kx-admin__live-list">
{data.liveEvents.map((e) => ( {LIVE_EVENTS.map((e) => (
<article key={e.eventId} className="kx-livecard"> <article key={e.id} className="kx-livecard">
<div className="kx-livecard__thumb" style={{ backgroundImage: `url(${THUMBS[e.thumbIdx]})` }} aria-hidden="true" />
<div className="kx-livecard__body"> <div className="kx-livecard__body">
<div className="kx-livecard__top"> <div className="kx-livecard__top">
<span className="kx-livecard__dot is-live" aria-hidden="true" /> <span className={`kx-livecard__dot ${e.live ? 'is-live' : 'is-prep'}`} aria-hidden="true" />
<StatusPill value={e.status} /> <span className="kx-livecard__state">{e.live ? 'LIVE NOW' : '준비중'}</span>
</div> </div>
<h3 className="kx-livecard__title">{e.name}</h3> <h3 className="kx-livecard__title">{e.title}</h3>
<p className="kx-livecard__meta">{e.hall ?? '홀 미배정'}</p> <p className="kx-livecard__meta">{e.hallTime}</p>
<div className="kx-livecard__occ"> {e.progress != null && (
<div className="kx-barlist__track"> <div className="kx-barlist__track">
<span className="kx-barlist__fill is-warning" style={{ width: `${clamp(e.occupancy)}%` }} /> <span className="kx-barlist__fill is-warning" style={{ width: `${e.progress}%` }} />
</div>
<span className="kx-livecard__occ-val tnum"> {clamp(e.occupancy)}%</span>
</div> </div>
)}
</div> </div>
</article> </article>
))} ))}
</div> </div>
)}
</aside> </aside>
</div> </div>
</> {/* 플로팅 AI 컨시어지 버튼: PLANNING M18 미포함 → P2/후속. 현재 비노출. */}
)}
</div> </div>
); );
} }
@ -162,44 +143,3 @@ const TOOLTIP_STYLE = {
border: '1px solid #E4E7EC', border: '1px solid #E4E7EC',
boxShadow: '0 4px 12px rgba(16,24,40,0.1)', boxShadow: '0 4px 12px rgba(16,24,40,0.1)',
} as const; } as const;
function AdminSkeleton() {
return (
<div aria-hidden="true" style={{ display: 'grid', gap: 16 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
{[0, 1, 2].map((i) => (
<Skeleton key={i} height={92} radius={12} />
))}
</div>
<Skeleton height={300} radius={12} />
</div>
);
}
function clamp(n: number): number {
return Math.max(0, Math.min(100, Math.round(n)));
}
function isDegradable(error: unknown): boolean {
return (
error instanceof ApiRequestError &&
(error.code === 'NETWORK' || error.code === 'NOT_FOUND' || error.code === 'NOT_IMPLEMENTED')
);
}
/**
* ( shape ) API. NETWORK/NOT_FOUND .
* 근거: 기존 sampleAdmin.ts(· ) AdminDashboardData .
*/
const FALLBACK: AdminDashboardData = {
kpis: ADMIN_KPIS.map((k) => ({ label: k.label, value: k.value, sub: k.delta ?? null })),
visitorTrend: VISITOR_TREND.map((p) => ({ hour: p.hour, count: p.today })),
liveEvents: LIVE_EVENTS.filter((e) => e.live).map((e) => ({
eventId: e.id,
name: e.title,
hall: e.hallTime,
occupancy: e.progress ?? 60,
status: 'ongoing',
})),
tenants: TENANTS,
};

View File

@ -147,20 +147,6 @@
font-size: var(--fs-caption); font-size: var(--fs-caption);
color: var(--color-neutral-500); color: var(--color-neutral-500);
} }
.kx-livecard__occ {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
}
.kx-livecard__occ .kx-barlist__track {
flex: 1;
}
.kx-livecard__occ-val {
font-size: var(--fs-caption);
color: var(--color-neutral-700);
white-space: nowrap;
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
.kx-admin__kpis { .kx-admin__kpis {

View File

@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { import {
Area, Area,
AreaChart, AreaChart,
@ -9,53 +8,40 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import { AiLabel } from '../../components/ui/Badge'; import { AiLabel, StatusBadge } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button'; import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { CHART } from '../chartColors'; import { CHART } from '../chartColors';
import { analyticsApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import type {
AnalyticsData,
AnalyticsKpi,
AnalyticsPeriod,
AnalyticsPerspective,
} from '../../api/types';
import { import {
EXHIBITOR_KPIS, EXHIBITOR_KPIS,
EXHIBITOR_PERF, EXHIBITOR_PERF,
EXHIBITOR_TREND,
GRADE_LABEL,
OPERATOR_KPIS, OPERATOR_KPIS,
OPERATOR_PNL, OPERATOR_PNL,
OPERATOR_TREND,
SECTORS, SECTORS,
type Kpi,
type Perspective,
type Period,
} from './sampleAnalytics'; } from './sampleAnalytics';
import './analytics.css'; import './analytics.css';
/* /*
* SCR-13 (M16 BI). Stitch business_intelligence . * SCR-13 (M16 BI). Stitch business_intelligence .
* 경로: GET /api/events/{eventId}/analytics?perspective&period ( · ·). * - (/) KPI·· .
* (/)· refetch. * 경계면: BI · (PLANNING §5A M16-1) .
* 폴백: NETWORK/NOT_FOUND sampleAnalytics . sectors·exhibitorPerf . * ( /analytics . P&L .)
*/ */
export function AnalyticsDashboardPage() { export function AnalyticsDashboardPage() {
const [perspective, setPerspective] = useState<AnalyticsPerspective>('operator'); const [perspective, setPerspective] = useState<Perspective>('operator');
const [period, setPeriod] = useState<AnalyticsPeriod>('event'); const [period, setPeriod] = useState<Period>('event');
const eventId = useResolvedEventId();
const isOperator = perspective === 'operator'; const isOperator = perspective === 'operator';
const kpis = isOperator ? OPERATOR_KPIS : EXHIBITOR_KPIS;
const q = useQuery({
queryKey: ['analytics', eventId, perspective, period],
queryFn: () => analyticsApi.get(eventId as string, perspective, period),
enabled: !!eventId,
retry: false,
});
const degraded = isDegradable(q.error);
const data: AnalyticsData | null = q.data ?? (degraded ? fallback(perspective) : null);
const hardError = q.isError && !degraded;
return ( return (
<div className="kx-bi"> <div className="kx-bi">
{/* 헤더 */}
<header className="kx-bi__head"> <header className="kx-bi__head">
<div> <div>
<h1 className="kx-bi__title"></h1> <h1 className="kx-bi__title"></h1>
@ -84,7 +70,7 @@ export function AnalyticsDashboardPage() {
className="kx-select" className="kx-select"
aria-label="분석 기간" aria-label="분석 기간"
value={period} value={period}
onChange={(e) => setPeriod(e.target.value as AnalyticsPeriod)} onChange={(e) => setPeriod(e.target.value as Period)}
> >
<option value="event"> </option> <option value="event"> </option>
<option value="d90"> 90</option> <option value="d90"> 90</option>
@ -95,69 +81,55 @@ export function AnalyticsDashboardPage() {
</div> </div>
</header> </header>
{!eventId && (
<EmptyState title="행사를 선택해 주세요" description="분석할 행사가 지정되지 않았습니다." />
)}
{degraded && (
<div className="kx-bi__degraded-bar"> <div className="kx-bi__degraded-bar">
<span className="kx-bi__degraded"> </span> <span className="kx-bi__degraded"> API ()</span>
API에 . BI · .
</div> </div>
)}
{q.isLoading && <BiSkeleton />}
{hardError && (
<ErrorState message="경영분석 집계를 불러오지 못했습니다." onRetry={() => q.refetch()} />
)}
{data && (
<>
{/* KPI 밴드 */} {/* KPI 밴드 */}
<section className="kx-bi__kpis" aria-label="핵심 지표"> <section className="kx-bi__kpis" aria-label="핵심 지표">
{data.kpis.length === 0 ? ( {kpis.map((k) => (
<EmptyState title="집계된 지표가 없습니다" /> <KpiCard key={k.key} kpi={k} />
) : ( ))}
data.kpis.map((k, i) => <KpiCard key={`${k.label}-${i}`} kpi={k} />)
)}
</section> </section>
{/* 히어로 추세(2/3) + 업종 구성(1/3) */} {/* 히어로 추세(2/3) + 업종 구성(1/3) */}
<div className="kx-bi__split"> <div className="kx-bi__split">
<section className="kx-card kx-bi__trend" aria-label="추세 차트"> <section className="kx-card kx-bi__trend" aria-label="추세 차트">
<div className="kx-card__head"> <div className="kx-card__head">
<h2> · </h2> <h2>{isOperator ? '매출 구성 추이' : '참가업체 vs 바이어 매칭 추이'}</h2>
<AiLabel> </AiLabel> <AiLabel>AI </AiLabel>
</div> </div>
<p className="kx-card__hint"> · ( 단위: ).</p> <p className="kx-card__hint"> AI (단위: 백만원 / ).</p>
<div className="kx-bi__chart"> <div className="kx-bi__chart">
{data.trend.length === 0 ? (
<EmptyState title="추이 데이터가 없습니다" description="집계 가능한 기간의 매출·부스 레코드가 없습니다." />
) : (
<ResponsiveContainer width="100%" height={320}> <ResponsiveContainer width="100%" height={320}>
<AreaChart data={data.trend} margin={{ top: 8, right: 12, bottom: 0, left: -8 }}> {isOperator ? (
<AreaChart data={OPERATOR_TREND} margin={{ top: 8, right: 12, bottom: 0, left: -8 }}>
<defs> <defs>
<linearGradient id="gRevenue" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="gRental" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={CHART.primary600} stopOpacity={0.35} /> <stop offset="0%" stopColor={CHART.primary600} stopOpacity={0.35} />
<stop offset="100%" stopColor={CHART.primary600} stopOpacity={0.02} /> <stop offset="100%" stopColor={CHART.primary600} stopOpacity={0.02} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid stroke={CHART.neutral200} strokeDasharray="3 3" vertical={false} /> <CartesianGrid stroke={CHART.neutral200} strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="period" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={{ stroke: CHART.neutral200 }} /> <XAxis dataKey="label" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={{ stroke: CHART.neutral200 }} />
<YAxis yAxisId="rev" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={54} tickFormatter={(v) => shortWon(v as number)} /> <YAxis tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={44} />
<YAxis yAxisId="booth" orientation="right" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={36} /> <Tooltip contentStyle={TOOLTIP_STYLE} />
<Tooltip <Area type="monotone" dataKey="auction" name="옥션 수수료" stackId="1" stroke={CHART.aiAccent} fill={CHART.aiAccent} fillOpacity={0.12} strokeWidth={2} />
contentStyle={TOOLTIP_STYLE} <Area type="monotone" dataKey="utility" name="유틸리티" stackId="1" stroke={CHART.slate} fill={CHART.slate} fillOpacity={0.12} strokeWidth={2} />
formatter={(value, name) => <Area type="monotone" dataKey="rental" name="임대" stackId="1" stroke={CHART.primary600} fill="url(#gRental)" strokeWidth={2} />
name === '매출' ? formatWon(value as number) : `${(value as number).toLocaleString()} 부스` </AreaChart>
} ) : (
/> <AreaChart data={EXHIBITOR_TREND} margin={{ top: 8, right: 12, bottom: 0, left: -8 }}>
<Area yAxisId="rev" type="monotone" dataKey="revenue" name="매출" stroke={CHART.primary600} fill="url(#gRevenue)" strokeWidth={2} /> <CartesianGrid stroke={CHART.neutral200} strokeDasharray="3 3" vertical={false} />
<Area yAxisId="booth" type="monotone" dataKey="booths" name="부스" stroke={CHART.aiAccent} fill={CHART.aiAccent} fillOpacity={0.08} strokeWidth={2} /> <XAxis dataKey="label" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={{ stroke: CHART.neutral200 }} />
<YAxis tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={44} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Area type="monotone" dataKey="exhibitors" name="참가업체" stroke={CHART.primary600} fill={CHART.primary600} fillOpacity={0.12} strokeWidth={2} />
<Area type="monotone" dataKey="buyers" name="바이어" stroke={CHART.aiAccent} fill={CHART.aiAccent} fillOpacity={0.1} strokeWidth={2} />
</AreaChart> </AreaChart>
</ResponsiveContainer>
)} )}
</ResponsiveContainer>
</div> </div>
</section> </section>
@ -165,76 +137,75 @@ export function AnalyticsDashboardPage() {
<div className="kx-card__head"> <div className="kx-card__head">
<h2> </h2> <h2> </h2>
</div> </div>
{data.sectors.length === 0 ? (
<EmptyState
title="업종 분류 미집계"
description="업종 분류 원천(M13)이 연동되면 표시됩니다."
/>
) : (
<ul className="kx-barlist"> <ul className="kx-barlist">
{data.sectors.map((s) => ( {SECTORS.map((s) => (
<li key={s.name} className="kx-barlist__item"> <li key={s.label} className="kx-barlist__item">
<div className="kx-barlist__top"> <div className="kx-barlist__top">
<span>{s.name}</span> <span>{s.label}</span>
<span className="tnum">{s.sharePercent.toFixed(1)}%</span> <span className="tnum">{s.pct}%</span>
</div> </div>
<div className="kx-barlist__track"> <div className="kx-barlist__track">
<span className="kx-barlist__fill" style={{ width: `${s.sharePercent}%` }} /> <span className="kx-barlist__fill" style={{ width: `${s.pct}%` }} />
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
)}
</section> </section>
</div> </div>
{/* 성과 테이블(전폭) */} {/* 성과 테이블(전폭) */}
<section className="kx-card kx-bi__table-card" aria-label="성과 테이블"> <section className="kx-card kx-bi__table-card" aria-label="성과 테이블">
<div className="kx-card__head"> <div className="kx-card__head">
<h2>{isOperator ? '손익(P&L) 구성' : '참가사 성과'}</h2> <h2>{isOperator ? '홀·행사별 손익(P&L)' : '상위 참가사 성과'}</h2>
{degraded && <span className="kx-bi__degraded"></span>} <span className="kx-bi__degraded"> API ()</span>
</div> </div>
<div className="kx-table-scroll"> <div className="kx-table-scroll">
{isOperator ? ( {isOperator ? (
data.pnl.length === 0 ? (
<EmptyState title="손익 항목이 없습니다" description="부스 임대·유틸리티 매출 레코드가 집계되면 표시됩니다." />
) : (
<table className="kx-table kx-table--zebra"> <table className="kx-table kx-table--zebra">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th className="kx-num"></th> <th></th>
<th></th> <th className="kx-num"></th>
<th className="kx-num"></th>
<th className="kx-num"></th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.pnl.map((r, i) => ( {OPERATOR_PNL.map((r) => (
<tr key={`${r.item}-${i}`}> <tr key={r.event}>
<td>{r.item}</td> <td>{r.event}</td>
<td className="kx-num tnum">{formatWon(r.amount)}</td> <td>{r.hall}</td>
<td className="kx-num tnum">{r.revenue}</td>
<td className="kx-num tnum">{r.contribution}</td>
<td className="kx-num tnum">{r.margin}</td>
<td><GradePill grade={r.grade} /></td> <td><GradePill grade={r.grade} /></td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
)
) : data.exhibitorPerf.length === 0 ? (
<EmptyState title="참가사 성과 미집계" description="리드·전환율 원천(M13 방문자)이 연동되면 표시됩니다." />
) : ( ) : (
<table className="kx-table kx-table--zebra"> <table className="kx-table kx-table--zebra">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th className="kx-num"></th> <th></th>
<th className="kx-num"></th> <th className="kx-num"> </th>
<th className="kx-num"> </th>
<th> </th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.exhibitorPerf.map((r, i) => ( {EXHIBITOR_PERF.map((r) => (
<tr key={`${r.companyName}-${i}`}> <tr key={r.company}>
<td>{r.companyName}</td> <td>{r.company}</td>
<td className="kx-num tnum">{r.leads.toLocaleString()}</td> <td className="tnum">{r.booth}</td>
<td className="kx-num tnum">{r.conversionPercent.toFixed(1)}%</td> <td className="kx-num tnum">{r.visits.toLocaleString()}</td>
<td className="kx-num tnum">{r.matches}</td>
<td><StatusBadge status={r.status} /></td>
<td><GradePill grade={r.grade} /></td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -242,8 +213,6 @@ export function AnalyticsDashboardPage() {
)} )}
</div> </div>
</section> </section>
</>
)}
</div> </div>
); );
} }
@ -255,102 +224,23 @@ const TOOLTIP_STYLE = {
boxShadow: '0 4px 12px rgba(16,24,40,0.1)', boxShadow: '0 4px 12px rgba(16,24,40,0.1)',
} as const; } as const;
function BiSkeleton() { function KpiCard({ kpi }: { kpi: Kpi }) {
return ( return (
<div aria-hidden="true" style={{ display: 'grid', gap: 16 }}> <div className={`kx-kpi kx-kpi--${kpi.tone ?? 'normal'}`}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}> <span className="kx-kpi__label">
{[0, 1, 2, 3].map((i) => ( {kpi.label}
<Skeleton key={i} height={92} radius={12} /> {kpi.ai && <span className="kx-kpi__ai"> AI</span>}
))} </span>
</div>
<Skeleton height={320} radius={12} />
<Skeleton height={220} radius={12} />
</div>
);
}
function KpiCard({ kpi }: { kpi: AnalyticsKpi }) {
const dir = kpi.trend === 'down' ? 'down' : 'up';
return (
<div className={`kx-kpi kx-kpi--${kpi.trend === 'down' ? 'warn' : 'normal'}`}>
<span className="kx-kpi__label">{kpi.label}</span>
<strong className="kx-kpi__value tnum">{kpi.value}</strong> <strong className="kx-kpi__value tnum">{kpi.value}</strong>
{kpi.delta && ( {kpi.delta && (
<span className={`kx-delta kx-delta--${dir}`}> <span className={`kx-delta kx-delta--${kpi.deltaDir ?? 'up'}`}>
{kpi.trend === 'down' ? '▼' : kpi.trend === 'up' ? '▲' : ''} {kpi.delta} {kpi.deltaDir === 'down' ? '▼' : '▲'} {kpi.delta}
</span> </span>
)} )}
</div> </div>
); );
} }
type GradeKey = 'excellent' | 'good' | 'fair' | 'review'; function GradePill({ grade }: { grade: 'excellent' | 'good' | 'fair' | 'review' }) {
const GRADE_MAP: Record<string, { key: GradeKey; label: string }> = { return <span className={`kx-grade kx-grade--${grade}`}>{GRADE_LABEL[grade]}</span>;
A: { key: 'excellent', label: 'A' },
B: { key: 'good', label: 'B' },
C: { key: 'fair', label: 'C' },
};
function GradePill({ grade }: { grade: string }) {
const g = GRADE_MAP[grade] ?? { key: 'review' as GradeKey, label: grade };
return <span className={`kx-grade kx-grade--${g.key}`}>{g.label}</span>;
}
/** 원 금액 → 억/만 단위 축약. */
function formatWon(v: number): string {
if (v >= 1e8) return `${(v / 1e8).toFixed(2)}`;
if (v >= 1e4) return `${Math.round(v / 1e4).toLocaleString()}`;
return `${v.toLocaleString()}`;
}
function shortWon(v: number): string {
if (v >= 1e8) return `${(v / 1e8).toFixed(0)}`;
if (v >= 1e4) return `${Math.round(v / 1e4)}`;
return String(v);
}
function isDegradable(error: unknown): boolean {
return (
error instanceof ApiRequestError &&
(error.code === 'NETWORK' || error.code === 'NOT_FOUND' || error.code === 'NOT_IMPLEMENTED')
);
}
/**
* ( shape ) API. NETWORK/NOT_FOUND .
* 근거: 기존 sampleAnalytics.ts(BI ) AnalyticsData .
*/
function fallback(perspective: AnalyticsPerspective): AnalyticsData {
const isOperator = perspective === 'operator';
const kpis = (isOperator ? OPERATOR_KPIS : EXHIBITOR_KPIS).map((k) => ({
label: k.label,
value: k.value,
delta: k.delta ?? null,
trend: k.deltaDir === 'down' ? 'down' : k.deltaDir === 'up' ? 'up' : 'flat',
}));
return {
kpis,
trend: [],
sectors: SECTORS.map((s) => ({ name: s.label, sharePercent: s.pct, revenue: 0 })),
pnl: isOperator
? OPERATOR_PNL.map((r) => ({
item: `${r.event} (${r.hall})`,
amount: parseWon(r.revenue),
grade: r.grade === 'excellent' ? 'A' : r.grade === 'good' ? 'B' : r.grade === 'fair' ? 'C' : 'C',
}))
: [],
exhibitorPerf: isOperator
? []
: EXHIBITOR_PERF.map((r) => ({
companyName: r.company,
leads: r.matches,
conversionPercent: r.visits > 0 ? (r.matches / r.visits) * 100 : 0,
})),
};
}
/** "₩412M" 류 표시 문자열 → 원 단위 근사(폴백 전용). */
function parseWon(s: string): number {
const n = parseFloat(s.replace(/[^0-9.]/g, ''));
if (Number.isNaN(n)) return 0;
if (/B/i.test(s)) return Math.round(n * 1e9);
if (/M/i.test(s)) return Math.round(n * 1e6);
return Math.round(n);
} }

View File

@ -1,33 +1,29 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { AiLabel } from '../../components/ui/Badge'; import { StatusBadge, AiLabel } from '../../components/ui/Badge';
import { AiImage } from '../../components/ui/AiImage'; import { AiImage } from '../../components/ui/AiImage';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { dashboardApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
import type { DashboardData } from '../../api/types';
import { StatusPill, timeAgo } from '../work/workShared';
import { import {
SAMPLE_EXHIBITORS, SAMPLE_EXHIBITORS,
SAMPLE_FEED, SAMPLE_FEED,
SAMPLE_KPIS, SAMPLE_KPIS,
SAMPLE_MILESTONES, SAMPLE_MILESTONES,
type Milestone,
} from './sampleDashboard'; } from './sampleDashboard';
import './dashboard.css'; import './dashboard.css';
// Stitch organizer_dashboard 번들 이미지(실 KINTEX 촬영본 — 히어로 시각화 데코, AI 샘플 라벨). // Stitch organizer_dashboard 번들 이미지(실 KINTEX 촬영본).
const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`; const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
const S7_AERIAL = `${MOCK}/img3.jpg`; const S7_AERIAL = `${MOCK}/img3.jpg`; // 홀 전경 조감(아이소메트릭 항공뷰)
const BOOTH_THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
/* /*
* SCR-02 . Stitch organizer_dashboard . * SCR-02 . Stitch organizer_dashboard .
* 경로: GET /api/events/{eventId}/dashboard ( milestones·kpis·exhibitors·feed). * - 히어로: D- + KPI 4 + + + S7 .
* 폴백: NETWORK/NOT_FOUND sampleDashboard (degraded ). UI. * 이슈: 계약에 /KPI// .
* ( ). * (···D-) .
*/ */
export function OrganizerDashboardPage() { export function OrganizerDashboardPage() {
const { eventId } = useParams(); useParams(); // eventId 스코프(라우팅 확인용)
const workspace = useAuthStore((s) => s.currentWorkspace()); const workspace = useAuthStore((s) => s.currentWorkspace());
const eventName = workspace?.eventName ?? '2026 스마트팩토리 코리아'; const eventName = workspace?.eventName ?? '2026 스마트팩토리 코리아';
@ -38,20 +34,9 @@ export function OrganizerDashboardPage() {
: '2026.08.11 ~ 08.14'; : '2026.08.11 ~ 08.14';
const dday = workspace?.dday ?? 31; const dday = workspace?.dday ?? 31;
const q = useQuery({
queryKey: ['dashboard', eventId],
queryFn: () => dashboardApi.get(eventId as string),
enabled: !!eventId,
retry: false,
});
const degraded = isDegradable(q.error);
const data: DashboardData | null = q.data ?? (degraded ? FALLBACK : null);
const hardError = q.isError && !degraded;
return ( return (
<div className="kx-dash"> <div className="kx-dash">
{/* 행사 헤더 — 인증 워크스페이스 실데이터 */} {/* 행사 헤더 */}
<header className="kx-dash__event"> <header className="kx-dash__event">
<div> <div>
<h1 className="kx-dash__event-name">{eventName}</h1> <h1 className="kx-dash__event-name">{eventName}</h1>
@ -62,68 +47,44 @@ export function OrganizerDashboardPage() {
<span className="kx-dash__countdown tnum"> D-{dday}</span> <span className="kx-dash__countdown tnum"> D-{dday}</span>
</header> </header>
{degraded && (
<div className="kx-dash__degraded-bar">
<span className="kx-dash__degraded"> </span>
API에 .
</div>
)}
{q.isLoading && <DashboardSkeleton />}
{hardError && (
<ErrorState
message="대시보드 집계를 불러오지 못했습니다."
onRetry={() => q.refetch()}
/>
)}
{data && (
<>
{/* 히어로: 마일스톤 타임라인 */} {/* 히어로: 마일스톤 타임라인 */}
<section className="kx-dash__hero" aria-label="마일스톤 타임라인"> <section className="kx-dash__hero" aria-label="마일스톤 타임라인">
{data.milestones.length === 0 ? (
<EmptyState title="파생 일정이 없습니다" description="행사 시작일이 확정되면 마일스톤이 표시됩니다." />
) : (
<ol className="kx-timeline"> <ol className="kx-timeline">
{data.milestones.map((m, i) => ( {SAMPLE_MILESTONES.map((m, i) => (
<li key={`${m.label}-${i}`} className={`kx-timeline__node is-${milestoneClass(m.state)}`}> <li key={m.key} className={`kx-timeline__node is-${m.state}`}>
{i < data.milestones.length - 1 && ( {i < SAMPLE_MILESTONES.length - 1 && (
<span className="kx-timeline__bar" aria-hidden="true" /> <span className="kx-timeline__bar" aria-hidden="true" />
)} )}
<span className="kx-timeline__dot" aria-hidden="true"> <span className="kx-timeline__dot" aria-hidden="true">
{m.state === 'done' ? '✓' : ''} {m.state === 'done' ? '✓' : ''}
</span> </span>
<div className="kx-timeline__label"> <div className="kx-timeline__label">
<span className="kx-timeline__d tnum">{ddayLabel(m.date)}</span> <span className="kx-timeline__d tnum">{m.dLabel}</span>
<span className="kx-timeline__title">{m.label}</span> <span className="kx-timeline__title">{m.title}</span>
</div> </div>
<div className="kx-timeline__card"> <div className="kx-timeline__card">
<span className="kx-timeline__muted">{stateLabel(m.state)}</span> {m.progress ? (
<span className={m.state === 'imminent' ? 'kx-timeline__imminent' : ''}>
{m.progress}
</span>
) : (
<span className="kx-timeline__muted">{stateLabel(m)}</span>
)}
</div> </div>
</li> </li>
))} ))}
</ol> </ol>
)}
</section> </section>
{/* KPI 카드 */} {/* KPI 카드 4개 */}
<section className="kx-dash__kpis" aria-label="핵심 지표"> <section className="kx-dash__kpis" aria-label="핵심 지표">
{data.kpis.length === 0 ? ( {SAMPLE_KPIS.map((k) => (
<EmptyState title="집계된 지표가 없습니다" /> <div key={k.key} className={`kx-kpi kx-kpi--${k.tone ?? 'normal'}`}>
) : (
data.kpis.map((k, i) => (
<div key={`${k.label}-${i}`} className={`kx-kpi kx-kpi--${kpiTone(k.trend)}`}>
<span className="kx-kpi__label">{k.label}</span> <span className="kx-kpi__label">{k.label}</span>
<strong className="kx-kpi__value tnum">{k.value}</strong> <strong className="kx-kpi__value tnum">{k.value}</strong>
{k.delta && ( {k.sub && <span className="kx-kpi__sub">{k.sub}</span>}
<span className={`kx-delta kx-delta--${k.trend === 'down' ? 'down' : 'up'}`}>
{k.trend === 'down' ? '▼' : k.trend === 'up' ? '▲' : ''} {k.delta}
</span>
)}
</div> </div>
)) ))}
)}
</section> </section>
{/* 하단: 참가업체 테이블(2/3) + 알림·S7(1/3) */} {/* 하단: 참가업체 테이블(2/3) + 알림·S7(1/3) */}
@ -131,53 +92,55 @@ export function OrganizerDashboardPage() {
<section className="kx-dash__table-wrap" aria-label="참가업체 현황"> <section className="kx-dash__table-wrap" aria-label="참가업체 현황">
<div className="kx-dash__section-head"> <div className="kx-dash__section-head">
<h2> </h2> <h2> </h2>
{degraded && <span className="kx-dash__degraded"></span>} <span className="kx-dash__degraded"> API ()</span>
</div> </div>
{data.exhibitors.length === 0 ? (
<EmptyState
title="참가업체 부스가 없습니다"
description="부스 배치·설계안이 등록되면 여기에 표시됩니다."
/>
) : (
<table className="kx-table"> <table className="kx-table">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th></th> <th></th>
<th> </th> <th> </th>
<th></th>
<th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.exhibitors.map((r, i) => ( {SAMPLE_EXHIBITORS.map((r, i) => (
<tr key={`${r.boothNo}-${i}`}> <tr key={r.boothNo}>
<td className="tnum">{r.boothNo}</td> <td className="tnum">{r.boothNo}</td>
<td>{r.companyName}</td> <td>{r.company}</td>
<td> <td>
<StatusPill value={r.status} /> <StatusBadge status={r.designStatus} />
</td>
<td>
<StatusBadge status={r.utilityStatus} />
</td>
<td>
<img
className="kx-table__thumb"
src={BOOTH_THUMBS[i % BOOTH_THUMBS.length]}
alt={`${r.company} 예상 사진 썸네일`}
loading="lazy"
/>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
)}
</section> </section>
<aside className="kx-dash__aside"> <aside className="kx-dash__aside">
<section className="kx-feed" aria-label="알림 피드"> <section className="kx-feed" aria-label="알림 피드">
<h2 className="kx-feed__title"></h2> <h2 className="kx-feed__title"></h2>
{data.feed.length === 0 ? (
<EmptyState title="새 알림이 없습니다" />
) : (
<ul className="kx-feed__list"> <ul className="kx-feed__list">
{data.feed.map((f) => ( {SAMPLE_FEED.map((f) => (
<li key={f.id} className={`kx-feed__item is-${feedKind(f.kind)}`}> <li key={f.key} className={`kx-feed__item is-${f.kind}`}>
<span className="kx-feed__dot" aria-hidden="true" /> <span className="kx-feed__dot" aria-hidden="true" />
<span className="kx-feed__text">{f.message}</span> <span className="kx-feed__text">{f.text}</span>
<span className="kx-feed__time">{timeAgo(f.at)}</span> <span className="kx-feed__time">{f.time}</span>
</li> </li>
))} ))}
</ul> </ul>
)}
</section> </section>
<section className="kx-dash__s7" aria-label="홀 전경 조감"> <section className="kx-dash__s7" aria-label="홀 전경 조감">
@ -197,90 +160,19 @@ export function OrganizerDashboardPage() {
</section> </section>
</aside> </aside>
</div> </div>
</>
)}
</div> </div>
); );
} }
function DashboardSkeleton() { function stateLabel(m: Milestone): string {
return ( switch (m.state) {
<div aria-hidden="true" style={{ display: 'grid', gap: 16 }}>
<Skeleton height={72} radius={12} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
{[0, 1, 2, 3].map((i) => (
<Skeleton key={i} height={92} radius={12} />
))}
</div>
<Skeleton height={260} radius={12} />
</div>
);
}
/** NETWORK/NOT_FOUND(백엔드 미배포·미접속) 만 샘플 폴백으로 강등. */
function isDegradable(error: unknown): boolean {
return (
error instanceof ApiRequestError &&
(error.code === 'NETWORK' || error.code === 'NOT_FOUND' || error.code === 'NOT_IMPLEMENTED')
);
}
function milestoneClass(state: string): string {
if (state === 'done') return 'done';
if (state === 'current') return 'imminent';
return 'upcoming';
}
function stateLabel(state: string): string {
switch (state) {
case 'done': case 'done':
return '완료'; return '완료';
case 'current': case 'current':
return '진행 중'; return '진행 중';
case 'imminent':
return '임박';
default: default:
return '예정'; return '예정';
} }
} }
function kpiTone(trend: string): 'normal' | 'warn' | 'error' {
return trend === 'down' ? 'warn' : 'normal';
}
function feedKind(kind: string): string {
if (kind === 'approval' || kind === 'approved') return 'approved';
if (kind === 'violation' || kind === 'rejected') return 'rejected';
if (kind === 'deadline') return 'deadline';
return 'submitted';
}
/** "YYYY-MM-DD" → D-데이 라벨(오늘 기준). */
function ddayLabel(dateStr: string): string {
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) return dateStr;
const today = new Date();
today.setHours(0, 0, 0, 0);
d.setHours(0, 0, 0, 0);
const diff = Math.round((d.getTime() - today.getTime()) / 86400000);
if (diff === 0) return 'D-DAY';
return diff > 0 ? `D-${diff}` : `D+${-diff}`;
}
/**
* ( shape ) API. NETWORK/NOT_FOUND .
* 근거: 기존 sampleDashboard.ts( ) DashboardData .
*/
const FALLBACK: DashboardData = {
milestones: SAMPLE_MILESTONES.map((m) => ({
label: m.title,
date: '',
state: m.state === 'imminent' ? 'current' : m.state === 'current' ? 'current' : m.state,
})),
kpis: SAMPLE_KPIS.map((k) => ({
label: k.label,
value: k.value,
delta: k.sub ?? null,
trend: k.tone === 'error' ? 'down' : k.tone === 'warn' ? 'down' : 'flat',
})),
exhibitors: SAMPLE_EXHIBITORS.map((e) => ({
companyName: e.company,
boothNo: e.boothNo,
status: e.designStatus,
})),
feed: SAMPLE_FEED.map((f) => ({ id: f.key, kind: f.kind, message: f.text, at: '' })),
};

View File

@ -199,17 +199,6 @@
padding: 2px 8px; padding: 2px 8px;
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
} }
.kx-dash__degraded-bar {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--fs-caption);
color: var(--color-neutral-700);
background: #fff8ef;
border: 1px solid #ffe4bf;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-lg);
}
/* 테이블 */ /* 테이블 */
.kx-table { .kx-table {

View File

@ -10,12 +10,10 @@ import { InviteDialog } from './InviteDialog';
import { SignupDialog } from './SignupDialog'; import { SignupDialog } from './SignupDialog';
import { ForgotPasswordDialog } from './ForgotPasswordDialog'; import { ForgotPasswordDialog } from './ForgotPasswordDialog';
import { LoginSlides } from './LoginSlides'; import { LoginSlides } from './LoginSlides';
import { OtpChallengePanel } from './OtpChallengePanel'; import type { WorkspaceDto } from '../../api/types';
import type { LoginResponse, WorkspaceDto } from '../../api/types';
import './login.css'; import './login.css';
type Phase = 'login' | 'otp' | 'workspace'; type Phase = 'login' | 'workspace';
type OtpMode = 'verify' | 'enroll';
// 아이디 기억 — 이메일만 저장(비번 저장 금지). // 아이디 기억 — 이메일만 저장(비번 저장 금지).
const REMEMBER_KEY = 'kintex_remember_email'; const REMEMBER_KEY = 'kintex_remember_email';
@ -35,8 +33,6 @@ export function LoginPage() {
const [inviteOpen, setInviteOpen] = useState(false); const [inviteOpen, setInviteOpen] = useState(false);
const [signupOpen, setSignupOpen] = useState(false); const [signupOpen, setSignupOpen] = useState(false);
const [forgotOpen, setForgotOpen] = useState(false); const [forgotOpen, setForgotOpen] = useState(false);
// 2FA 챌린지 — 1단계 성공 후 OTP 단계로 전환.
const [otpChallenge, setOtpChallenge] = useState<{ token: string; mode: OtpMode } | null>(null);
// 아이디 기억 프리필 — 저장된 이메일이 있으면 채우고 체크 유지. // 아이디 기억 프리필 — 저장된 이메일이 있으면 채우고 체크 유지.
useEffect(() => { useEffect(() => {
@ -47,44 +43,24 @@ export function LoginPage() {
} }
}, []); }, []);
// 최종 로그인 확정(공통) — 토큰 적용 + 워크스페이스 단계 이동.
function completeLogin(res: LoginResponse) {
if (remember) localStorage.setItem(REMEMBER_KEY, email.trim());
else localStorage.removeItem(REMEMBER_KEY);
applyLogin(res);
setOtpChallenge(null);
setPhase('workspace');
}
async function handleLogin(e: React.FormEvent) { async function handleLogin(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
setError(null); setError(null);
setSubmitting(true); setSubmitting(true);
try { try {
// 2FA/잠금 적용 1단계 — status 로 분기. const res = await authApi.login({ email, password });
const res = await authApi.secureLogin({ email, password }); if (remember) localStorage.setItem(REMEMBER_KEY, email.trim());
if (res.status === 'OK' && res.login) { else localStorage.removeItem(REMEMBER_KEY);
completeLogin(res.login); applyLogin(res);
} else if (res.challengeToken) { setPhase('workspace');
// OTP_REQUIRED(등록자 6자리) | OTP_ENROLL(미등록 등록 강제).
setOtpChallenge({
token: res.challengeToken,
mode: res.status === 'OTP_ENROLL' ? 'enroll' : 'verify',
});
setPhase('otp');
} else {
setError('로그인 응답을 처리하지 못했습니다. 다시 시도해 주세요.');
}
} catch (err) { } catch (err) {
if (err instanceof ApiRequestError) { if (err instanceof ApiRequestError) {
// 잠금·미구현·자격오류 등 코드별 안내(계정 존재 여부 미노출). // 계약: login은 매퍼 대기 시 501(NOT_IMPLEMENTED) — degraded 안내
if (err.code === 'ACCOUNT_LOCKED') { setError(
setError('로그인 시도가 일시적으로 제한되었습니다. 잠시 후 다시 시도해 주세요.'); err.code === 'NOT_IMPLEMENTED'
} else if (err.code === 'NOT_IMPLEMENTED') { ? '로그인 서비스가 준비 중입니다. 잠시 후 다시 시도해 주세요.'
setError('로그인 서비스가 준비 중입니다. 잠시 후 다시 시도해 주세요.'); : err.message,
} else { );
setError(err.message);
}
} else { } else {
setError('로그인 중 오류가 발생했습니다.'); setError('로그인 중 오류가 발생했습니다.');
} }
@ -99,28 +75,6 @@ export function LoginPage() {
navigate(`/events/${ws.eventId}/halls/H7/layout`); navigate(`/events/${ws.eventId}/halls/H7/layout`);
} }
if (phase === 'otp' && otpChallenge) {
return (
<div className="kx-login">
<LoginSlides />
<section className="kx-login__panel">
<OtpChallengePanel
challengeToken={otpChallenge.token}
mode={otpChallenge.mode}
onSuccess={completeLogin}
onCancel={() => {
setOtpChallenge(null);
setPassword('');
setError(null);
setPhase('login');
}}
/>
<p className="kx-login__copy">© 2026 KINTEX. All rights reserved.</p>
</section>
</div>
);
}
if (phase === 'workspace') { if (phase === 'workspace') {
return ( return (
<div className="kx-ws"> <div className="kx-ws">

View File

@ -1,136 +0,0 @@
import { useEffect, useState } from 'react';
import { authApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
import { Button } from '../../components/ui/Button';
import type { LoginResponse, OtpSetupResponse } from '../../api/types';
/**
* 2 OTP LoginPage 1 .
* - mode="verify" : OTP 6 .
* - mode="enroll" : setup(QR·릿) 6 ·.
* challengeToken (5)·1 ( JWT ).
*/
interface Props {
challengeToken: string;
mode: 'verify' | 'enroll';
onSuccess: (res: LoginResponse) => void;
onCancel: () => void;
}
export function OtpChallengePanel({ challengeToken, mode, onSuccess, onCancel }: Props) {
const [setup, setSetup] = useState<OtpSetupResponse | null>(null);
const [code, setCode] = useState('');
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [loadingSetup, setLoadingSetup] = useState(mode === 'enroll');
// 등록 강제 모드 — 진입 시 시크릿·QR 발급.
useEffect(() => {
if (mode !== 'enroll') return;
let alive = true;
(async () => {
try {
const res = await authApi.otpEnrollSetup(challengeToken);
if (alive) setSetup(res);
} catch (err) {
if (alive) setError(messageOf(err));
} finally {
if (alive) setLoadingSetup(false);
}
})();
return () => {
alive = false;
};
}, [mode, challengeToken]);
async function submit(e: React.FormEvent) {
e.preventDefault();
if (code.length !== 6) return;
setError(null);
setBusy(true);
try {
const res =
mode === 'enroll'
? await authApi.otpEnrollVerify(challengeToken, code)
: await authApi.otpVerify(challengeToken, code);
onSuccess(res);
} catch (err) {
setError(messageOf(err));
setCode('');
} finally {
setBusy(false);
}
}
return (
<div className="kx-login__card">
<h2 className="kx-login__title">2 </h2>
<p className="kx-login__title-sub">
{mode === 'enroll'
? '보안 정책에 따라 OTP(Authenticator) 등록이 필요합니다.'
: 'Authenticator 앱에 표시된 6자리 코드를 입력하세요.'}
</p>
{mode === 'enroll' && (
<div className="kx-otp__enroll">
{loadingSetup && <p className="kx-login__title-sub">QR </p>}
{setup && (
<>
<ol className="kx-otp__steps">
<li>Google/Microsoft Authenticator QR을 .</li>
<li> .</li>
<li> 6 .</li>
</ol>
<div className="kx-otp__qr">
<img src={setup.qrImageDataUri} alt="OTP QR" width={180} height={180} />
</div>
<div className="kx-otp__secret">
<span className="kx-otp__secret-label"> </span>
<code>{setup.secret}</code>
</div>
</>
)}
</div>
)}
<form className="kx-login__form" onSubmit={submit}>
<div className="kx-field">
<label htmlFor="otp-code"> (6)</label>
<input
id="otp-code"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
placeholder="000000"
className="kx-otp__code-input"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
autoFocus
/>
</div>
{error && (
<p className="kx-login__error" role="alert">
{error}
</p>
)}
<Button type="submit" block disabled={busy || code.length !== 6}>
{busy ? '확인 중…' : mode === 'enroll' ? '등록 완료 · 로그인' : '인증 · 로그인'}
</Button>
<button type="button" className="kx-login__text-link kx-otp__cancel" onClick={onCancel}>
</button>
</form>
</div>
);
}
function messageOf(err: unknown): string {
if (err instanceof ApiRequestError) {
if (err.code === 'OTP_INVALID') return '코드가 올바르지 않습니다. 다시 시도해 주세요.';
if (err.code === 'OTP_REQUIRED') return '인증 세션이 만료되었습니다. 다시 로그인해 주세요.';
return err.message;
}
return '인증 중 오류가 발생했습니다.';
}

View File

@ -1,168 +0,0 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { authApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
import { Button } from '../../components/ui/Button';
import type { OtpSetupResponse } from '../../api/types';
import './login.css';
/**
* 2 (Authenticator/TOTP) (/otp-setup).
* setup(QR)·6 confirm . .
*/
type Phase = 'loading' | 'idle' | 'setup' | 'enabled';
export function OtpSetupPage() {
const navigate = useNavigate();
const [phase, setPhase] = useState<Phase>('loading');
const [data, setData] = useState<OtpSetupResponse | null>(null);
const [code, setCode] = useState('');
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
useEffect(() => {
let alive = true;
(async () => {
try {
const st = await authApi.otpStatus();
if (alive) setPhase(st.otpEnabled ? 'enabled' : 'idle');
} catch (err) {
if (alive) {
setError(messageOf(err));
setPhase('idle');
}
}
})();
return () => {
alive = false;
};
}, []);
async function startSetup() {
setError(null);
setBusy(true);
try {
const res = await authApi.otpSetup();
setData(res);
setPhase('setup');
} catch (err) {
setError(messageOf(err));
} finally {
setBusy(false);
}
}
async function confirm() {
setError(null);
setBusy(true);
try {
await authApi.otpConfirm(code.trim());
setData(null);
setCode('');
setPhase('enabled');
} catch (err) {
setError(messageOf(err));
} finally {
setBusy(false);
}
}
async function disable() {
setError(null);
setBusy(true);
try {
await authApi.otpResetSelf();
setPhase('idle');
} catch (err) {
setError(messageOf(err));
} finally {
setBusy(false);
}
}
return (
<div className="kx-otp-page">
<div className="kx-login__card">
<h2 className="kx-login__title">2 Authenticator(OTP)</h2>
<p className="kx-login__title-sub">
Google·Microsoft Authenticator OTP 6 2 .
</p>
{error && (
<p className="kx-login__error" role="alert">
{error}
</p>
)}
{phase === 'loading' && <p className="kx-login__title-sub"> </p>}
{phase === 'idle' && (
<div className="kx-otp__enroll">
<p className="kx-login__title-sub"> QR .</p>
<Button block disabled={busy} onClick={startSetup}>
{busy ? '발급 중…' : 'Authenticator 등록 시작'}
</Button>
</div>
)}
{phase === 'setup' && data && (
<div className="kx-otp__enroll">
<ol className="kx-otp__steps">
<li>Authenticator QR을 .</li>
<li> .</li>
<li> 6 .</li>
</ol>
<div className="kx-otp__qr">
<img src={data.qrImageDataUri} alt="OTP QR" width={180} height={180} />
</div>
<div className="kx-otp__secret">
<span className="kx-otp__secret-label"> </span>
<code>{data.secret}</code>
</div>
<div className="kx-field">
<label htmlFor="otp-confirm"> 6 </label>
<input
id="otp-confirm"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
placeholder="000000"
className="kx-otp__code-input"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
/>
</div>
<Button block disabled={busy || code.length !== 6} onClick={confirm}>
{busy ? '확인 중…' : '코드 확인 · 활성화'}
</Button>
</div>
)}
{phase === 'enabled' && (
<div className="kx-otp__enroll">
<p className="kx-otp__ok">2 (Authenticator) .</p>
<Button variant="secondary" block disabled={busy} onClick={disable}>
{busy ? '처리 중…' : 'Authenticator 해제(이메일 인증으로 복귀)'}
</Button>
</div>
)}
<button
type="button"
className="kx-login__text-link kx-otp__cancel"
onClick={() => navigate(-1)}
>
</button>
</div>
</div>
);
}
function messageOf(err: unknown): string {
if (err instanceof ApiRequestError) {
if (err.code === 'OTP_INVALID') return '코드가 올바르지 않습니다. 다시 시도해 주세요.';
return err.message;
}
return 'OTP 처리 중 오류가 발생했습니다.';
}

View File

@ -494,67 +494,3 @@
font-size: var(--fs-body); font-size: var(--fs-body);
color: var(--color-neutral-700); color: var(--color-neutral-700);
} }
/* ── 2차 인증(OTP) 패널 ─────────────────────────────────────────── */
.kx-otp-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
background: var(--color-neutral-050);
}
.kx-otp__enroll {
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.kx-otp__steps {
margin: 0;
padding-left: 18px;
font-size: var(--fs-caption);
line-height: 1.8;
color: var(--color-neutral-700);
}
.kx-otp__qr {
text-align: center;
}
.kx-otp__qr img {
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
background: var(--color-white);
padding: 8px;
}
.kx-otp__secret {
display: flex;
flex-direction: column;
gap: 4px;
}
.kx-otp__secret-label {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
}
.kx-otp__secret code {
display: block;
word-break: break-all;
background: var(--color-neutral-050);
border: 1px solid var(--color-neutral-200);
color: var(--color-neutral-900);
padding: 8px 10px;
border-radius: 6px;
font-size: var(--fs-caption);
}
.kx-otp__code-input {
letter-spacing: 6px;
text-align: center;
font-size: 20px;
}
.kx-otp__ok {
font-size: var(--fs-body);
color: var(--color-primary-700);
}
.kx-otp__cancel {
align-self: center;
margin-top: var(--space-2);
}

View File

@ -1,45 +1,31 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import {
import { occupancyTone } from '../chartColors'; PolarAngleAxis,
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States'; RadialBar,
import { opsApi } from '../../api/endpoints'; RadialBarChart,
import { ApiRequestError } from '../../api/client'; ResponsiveContainer,
import { useResolvedEventId } from '../../hooks/useResolvedEventId'; } from 'recharts';
import type { OpsData } from '../../api/types'; import { CHART, occupancyTone } from '../chartColors';
import { import {
HALL_CELLS, HALL_CELLS,
HVAC as SAMPLE_HVAC, HALL_GROUPS,
HVAC,
LIGHTING_ZONES, LIGHTING_ZONES,
PARKING_ZONES, PARKING_ZONES,
TOTAL_EST_PEOPLE,
} from './sampleOps'; } from './sampleOps';
import './ops.css'; import './ops.css';
/* /*
* SCR-16 (M14). Stitch hall_operations . * SCR-14 (M14). Stitch hall_operations .
* 경로: GET /api/events/{eventId}/ops ( + HVAC//). * - 히어로: 혼잡도 ( ) + HVAC· + .
* ·IoT (PLANNING M14) estPeople=0·hvac=null·/ . * 경계면: 센서·IoT . (§2.6).
* UI로 ( ).
* 폴백: NETWORK/NOT_FOUND sampleOps .
*/ */
export function HallOperationsPage() { export function HallOperationsPage() {
const [mode, setMode] = useState<'live' | 'history'>('live'); const [mode, setMode] = useState<'live' | 'history'>('live');
const eventId = useResolvedEventId(); const [group, setGroup] = useState(HALL_GROUPS[0].id);
const q = useQuery({ const powerPct = Math.round((HVAC.powerLoadMw / HVAC.powerCapacityMw) * 100);
queryKey: ['ops', eventId],
queryFn: () => opsApi.get(eventId as string),
enabled: !!eventId,
retry: false,
});
const degraded = isDegradable(q.error);
const data: OpsData | null = q.data ?? (degraded ? FALLBACK : null);
const hardError = q.isError && !degraded;
const hvac = data?.hvac;
const hasHvac = !!hvac && (hvac.tempC != null || hvac.humidityPercent != null || hvac.co2Ppm != null);
const totalPeople = data?.halls.reduce((s, h) => s + h.estPeople, 0) ?? 0;
const hasPeople = totalPeople > 0;
return ( return (
<div className="kx-ops"> <div className="kx-ops">
@ -49,58 +35,55 @@ export function HallOperationsPage() {
<p className="kx-ops__subtitle"> </p> <p className="kx-ops__subtitle"> </p>
</div> </div>
<div className="kx-seg" role="tablist" aria-label="조회 모드"> <div className="kx-seg" role="tablist" aria-label="조회 모드">
<button role="tab" aria-selected={mode === 'live'} className={`kx-seg__btn ${mode === 'live' ? 'is-active' : ''}`} onClick={() => setMode('live')}> <button
role="tab"
aria-selected={mode === 'live'}
className={`kx-seg__btn ${mode === 'live' ? 'is-active' : ''}`}
onClick={() => setMode('live')}
>
</button> </button>
<button role="tab" aria-selected={mode === 'history'} className={`kx-seg__btn ${mode === 'history' ? 'is-active' : ''}`} onClick={() => setMode('history')}> <button
role="tab"
aria-selected={mode === 'history'}
className={`kx-seg__btn ${mode === 'history' ? 'is-active' : ''}`}
onClick={() => setMode('history')}
>
</button> </button>
</div> </div>
</header> </header>
{!eventId && (
<EmptyState title="행사를 선택해 주세요" description="현장 운영을 조회할 행사가 지정되지 않았습니다." />
)}
{degraded && (
<div className="kx-ops__degraded-bar">
<span className="kx-ops__degraded-badge"> </span>
API에 .
</div>
)}
{q.isLoading && <OpsSkeleton />}
{hardError && (
<ErrorState message="현장 운영 현황을 불러오지 못했습니다." onRetry={() => q.refetch()} />
)}
{data && (
<div className="kx-ops__grid"> <div className="kx-ops__grid">
{/* 히어로: 혼잡도 */} {/* 히어로: 혼잡도 히트맵 (span 8) */}
<section className="kx-card kx-ops__heat" aria-label="홀별 현황"> <section className="kx-card kx-ops__heat" aria-label="실시간 혼잡도 히트맵">
<div className="kx-card__head"> <div className="kx-card__head">
<h2> </h2> <h2> </h2>
<select
className="kx-select"
aria-label="홀 그룹 선택"
value={group}
onChange={(e) => setGroup(e.target.value)}
>
{HALL_GROUPS.map((g) => (
<option key={g.id} value={g.id}>
{g.label}
</option>
))}
</select>
</div> </div>
<span className="kx-ops__sensor-badge"> <span className="kx-ops__sensor-badge"> · (M10) /</span>
{hasPeople ? '체크인(M10) 파생' : '센서 미연동 · 배정 홀 목록'}
</span>
{data.halls.length === 0 ? (
<EmptyState title="배정된 홀이 없습니다" description="행사에 홀이 배정되면 표시됩니다." />
) : (
<>
<div className="kx-ops__halls"> <div className="kx-ops__halls">
{data.halls.map((c) => ( {HALL_CELLS.map((c) => (
<div key={c.hallId} className={`kx-ops__hall is-${heatClass(c.heat)}`}> <div key={c.id} className={`kx-ops__hall is-${c.level}`}>
<span className="kx-ops__hall-label">{c.label}</span> <span className="kx-ops__hall-label">{c.label}</span>
{c.heat === 'busy' && <span className="kx-ops__hall-tag"></span>} {c.level === 'busy' && <span className="kx-ops__hall-tag"></span>}
<span className="kx-ops__hall-people tnum"> <span className="kx-ops__hall-people tnum">{c.estPeople.toLocaleString()}</span>
{c.estPeople > 0 ? `${c.estPeople.toLocaleString()}` : '—'}
</span>
</div> </div>
))} ))}
</div> </div>
<div className="kx-ops__heat-foot"> <div className="kx-ops__heat-foot">
<ul className="kx-ops__legend"> <ul className="kx-ops__legend">
<li><span className="kx-dot is-smooth" /> </li> <li><span className="kx-dot is-smooth" /> </li>
@ -108,131 +91,103 @@ export function HallOperationsPage() {
<li><span className="kx-dot is-busy" /> </li> <li><span className="kx-dot is-busy" /> </li>
</ul> </ul>
<span className="kx-ops__total"> <span className="kx-ops__total">
{hasPeople ? ( <strong className="tnum">{TOTAL_EST_PEOPLE.toLocaleString()}</strong>
<> <strong className="tnum">{totalPeople.toLocaleString()}</strong></>
) : (
<> </>
)}
</span> </span>
</div> </div>
</>
)}
</section> </section>
{/* 우측: HVAC + 조명존 */} {/* 우측: HVAC + 조명존 (span 4) */}
<aside className="kx-ops__aside"> <aside className="kx-ops__aside">
<section className="kx-card" aria-label="공조(HVAC)"> <section className="kx-card" aria-label="HVAC 최적화">
<div className="kx-card__head"> <div className="kx-card__head">
<h2> (HVAC)</h2> <h2>HVAC </h2>
<span className="kx-ops__pill is-ok">{HVAC.status}</span>
</div>
<div className="kx-ops__hvac">
<div className="kx-ops__gauge">
<ResponsiveContainer width="100%" height={140}>
<RadialBarChart
innerRadius="70%"
outerRadius="100%"
data={[{ name: '전력 부하', value: powerPct }]}
startAngle={90}
endAngle={-270}
>
<PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
<RadialBar dataKey="value" cornerRadius={8} fill={CHART.primary600} background={{ fill: CHART.neutral100 }} />
</RadialBarChart>
</ResponsiveContainer>
<div className="kx-ops__gauge-label">
<strong className="tnum">{HVAC.powerLoadMw}MW</strong>
<span> </span>
</div>
</div> </div>
{!hasHvac ? (
<EmptyState title="공조 계측 미연동" description="HVAC 센서가 연동되면 온·습도·CO₂가 표시됩니다." />
) : (
<div className="kx-ops__hvac-meta"> <div className="kx-ops__hvac-meta">
<div> <div>
<span className="kx-ops__meta-k"></span> <span className="kx-ops__meta-k"> </span>
<strong className="tnum">{hvac!.tempC != null ? `${hvac!.tempC}°C` : '—'}</strong> <strong className="tnum">{HVAC.avgTempC}°C</strong>
</div> </div>
<div> <div>
<span className="kx-ops__meta-k"></span> <span className="kx-ops__meta-k"></span>
<strong className="tnum">{hvac!.humidityPercent != null ? `${hvac!.humidityPercent}%` : '—'}</strong> <strong className="tnum">{powerPct}%</strong>
</div> </div>
<div>
<span className="kx-ops__meta-k">CO</span>
<strong className="tnum">{hvac!.co2Ppm != null ? `${hvac!.co2Ppm}ppm` : '—'}</strong>
</div> </div>
</div> </div>
)}
</section> </section>
<section className="kx-card" aria-label="조명 존"> <section className="kx-card" aria-label="조명 존">
<div className="kx-card__head"> <div className="kx-card__head">
<h2> </h2> <h2> </h2>
<label className="kx-switch">
<input type="checkbox" defaultChecked aria-label="자동 디밍" />
<span className="kx-switch__track" aria-hidden="true" />
<span className="kx-switch__text"> </span>
</label>
</div> </div>
{data.lighting.length === 0 ? (
<EmptyState title="조명 IoT 미연동" description="조명 제어 존이 연동되면 점등률이 표시됩니다." />
) : (
<ul className="kx-barlist"> <ul className="kx-barlist">
{data.lighting.map((z) => ( {LIGHTING_ZONES.map((z) => (
<li key={z.zone} className="kx-barlist__item"> <li key={z.label} className="kx-barlist__item">
<div className="kx-barlist__top"> <div className="kx-barlist__top">
<span>{z.zone}</span> <span>{z.label}</span>
<span className="tnum">{z.onPercent}%</span> <span className="tnum">{z.pct}%</span>
</div> </div>
<div className="kx-barlist__track"> <div className="kx-barlist__track">
<span className="kx-barlist__fill" style={{ width: `${z.onPercent}%` }} /> <span className="kx-barlist__fill" style={{ width: `${z.pct}%` }} />
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
)}
</section> </section>
</aside> </aside>
{/* 하단: 주차 점유 */} {/* 하단: 주차 점유 (span 12) */}
<section className="kx-card kx-ops__parking" aria-label="주차 점유 현황"> <section className="kx-card kx-ops__parking" aria-label="주차 점유 현황">
<div className="kx-card__head"> <div className="kx-card__head">
<h2> </h2> <h2> </h2>
<span className="kx-ops__note">iparking ()</span>
</div> </div>
{data.parking.length === 0 ? (
<EmptyState title="주차 연계 미연동" description="iparking 연계가 활성화되면 존별 점유가 표시됩니다." />
) : (
<div className="kx-ops__zones"> <div className="kx-ops__zones">
{data.parking.map((z) => { {PARKING_ZONES.map((z) => {
const pct = z.capacity > 0 ? Math.round((z.occupancy / z.capacity) * 100) : 0; const pct = Math.round((z.occupied / z.total) * 100);
const tone = occupancyTone(pct); const tone = occupancyTone(pct);
return ( return (
<div key={z.zone} className="kx-ops__zone"> <div key={z.id} className="kx-ops__zone">
<div className="kx-ops__zone-top"> <div className="kx-ops__zone-top">
<span className="kx-ops__zone-label">{z.zone}</span> <span className="kx-ops__zone-label">{z.label}</span>
<strong className={`kx-ops__zone-pct tnum is-${tone}`}>{pct}%</strong> <strong className={`kx-ops__zone-pct tnum is-${tone}`}>{pct}%</strong>
</div> </div>
<div className="kx-barlist__track"> <div className="kx-barlist__track">
<span className={`kx-barlist__fill is-${tone}`} style={{ width: `${pct}%` }} /> <span className={`kx-barlist__fill is-${tone}`} style={{ width: `${pct}%` }} />
</div> </div>
<span className="kx-ops__zone-count tnum"> <span className="kx-ops__zone-count tnum">
{z.occupancy.toLocaleString()} / {z.capacity.toLocaleString()} {z.occupied.toLocaleString()} / {z.total.toLocaleString()}
</span> </span>
</div> </div>
); );
})} })}
</div> </div>
)}
</section> </section>
</div> </div>
)}
</div> </div>
); );
} }
function OpsSkeleton() {
return (
<div aria-hidden="true" style={{ display: 'grid', gap: 16 }}>
<Skeleton height={220} radius={12} />
<Skeleton height={160} radius={12} />
</div>
);
}
function heatClass(heat: string): string {
if (heat === 'busy' || heat === 'moderate' || heat === 'smooth') return heat;
return 'smooth';
}
function isDegradable(error: unknown): boolean {
return (
error instanceof ApiRequestError &&
(error.code === 'NETWORK' || error.code === 'NOT_FOUND' || error.code === 'NOT_IMPLEMENTED')
);
}
/**
* ( shape ) API. NETWORK/NOT_FOUND .
* 근거: 기존 sampleOps.ts(·IoT ) OpsData .
*/
const FALLBACK: OpsData = {
halls: HALL_CELLS.map((c) => ({ hallId: c.id, label: c.label, estPeople: c.estPeople, heat: c.level })),
hvac: { tempC: SAMPLE_HVAC.avgTempC, humidityPercent: null, co2Ppm: null },
lighting: LIGHTING_ZONES.map((z) => ({ zone: z.label, onPercent: z.pct })),
parking: PARKING_ZONES.map((z) => ({ zone: z.label, occupancy: z.occupied, capacity: z.total })),
};

View File

@ -64,25 +64,6 @@
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
} }
.kx-ops__degraded-bar {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--fs-caption);
color: var(--color-neutral-700);
background: #fff8ef;
border: 1px solid #ffe4bf;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-lg);
}
.kx-ops__degraded-badge {
font-size: 11px;
color: var(--color-warning);
background: #fff4e5;
padding: 2px 8px;
border-radius: var(--radius-pill);
}
/* 히트맵 홀 셀 */ /* 히트맵 홀 셀 */
.kx-ops__halls { .kx-ops__halls {
flex: 1; flex: 1;

View File

@ -1,11 +1,8 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useAuthStore } from '../../store/authStore';
import { ApiRequestError } from '../../api/client';
import { exhibitionApi } from '../../api/endpoints';
import type { ExhibitionDto } from '../../api/types';
import { StatusBadge } from '../../components/ui/Badge'; import { StatusBadge } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button'; import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States'; import { EmptyState } from '../../components/ui/States';
import { import {
IconCalendar, IconCalendar,
IconChevronLeft, IconChevronLeft,
@ -21,7 +18,6 @@ import {
eventDaysFor, eventDaysFor,
eventsByDateFor, eventsByDateFor,
hallUtilizationFor, hallUtilizationFor,
type EventCategory,
type EventStatus, type EventStatus,
type ScheduleEvent, type ScheduleEvent,
} from './sampleSchedule'; } from './sampleSchedule';
@ -30,42 +26,6 @@ import './schedule.css';
const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`; const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`]; const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
const TODAY = new Date(2026, 6, 11); // 2026-07-11 (currentDate) const TODAY = new Date(2026, 6, 11); // 2026-07-11 (currentDate)
const TODAY_YMD = '2026-07-11';
const KNOWN_CATEGORIES = new Set<EventCategory>(['exhibition', 'culture', 'trade', 'tech']);
/** 날짜 기준 상태 재계산 — DB status 원문 대신 TODAY 기준 진행/예정/종료로 정규화. */
function statusFromDates(start: string, end: string): EventStatus {
if (end < TODAY_YMD) return 'ended';
if (start > TODAY_YMD) return 'upcoming';
return 'ongoing';
}
/** "2026-08-11","2026-08-14" → "2026.08.1108.14" */
function periodLabel(start: string, end: string): string {
const s = start.split('-').join('.');
const e = end.split('-').join('.');
return e.startsWith(s.slice(0, 5)) ? `${s}${e.slice(5)}` : `${s}${e}`;
}
/** 실 API ExhibitionDto → 화면 ScheduleEvent 매핑. DB 미포함 필드(카테고리·홀·인원)는 기본값. */
function toScheduleEvent(e: ExhibitionDto, i: number): ScheduleEvent {
const category = (KNOWN_CATEGORIES.has(e.category as EventCategory)
? (e.category as EventCategory)
: 'exhibition') as EventCategory;
return {
id: e.id,
title: e.name,
category,
status: statusFromDates(e.startDate, e.endDate),
period: periodLabel(e.startDate, e.endDate),
start: e.startDate,
end: e.endDate,
halls: e.hallLabel ?? '홀 미배정',
estVisitors: e.estVisitors != null ? `${e.estVisitors.toLocaleString()}` : '집계 대기',
thumbIdx: i % THUMBS.length,
};
}
/** 홀 필터 매칭 — halls 문자열("홀 4, 5A"·"제2전시장 홀7")에서 홀 번호만 추출해 전시장 범위("1-5"/"6-10")와 대조. "제N전시장"의 N·"7A-1"의 -1 오매칭 방지. */ /** 홀 필터 매칭 — halls 문자열("홀 4, 5A"·"제2전시장 홀7")에서 홀 번호만 추출해 전시장 범위("1-5"/"6-10")와 대조. "제N전시장"의 N·"7A-1"의 -1 오매칭 방지. */
function matchesHall(halls: string, range: string): boolean { function matchesHall(halls: string, range: string): boolean {
@ -87,6 +47,7 @@ function matchesHall(halls: string, range: string): boolean {
* 경계면: 행사 workspaces( ) , . * 경계면: 행사 workspaces( ) , .
*/ */
export function ExhibitionSchedulePage() { export function ExhibitionSchedulePage() {
const workspaces = useAuthStore((s) => s.workspaces);
const [view, setView] = useState<'list' | 'calendar'>('list'); const [view, setView] = useState<'list' | 'calendar'>('list');
const [year, setYear] = useState('2026'); const [year, setYear] = useState('2026');
const [hall, setHall] = useState('all'); const [hall, setHall] = useState('all');
@ -95,31 +56,24 @@ export function ExhibitionSchedulePage() {
// 우측 미니 캘린더·홀 가동률 공유 월 커서 // 우측 미니 캘린더·홀 가동률 공유 월 커서
const [asideCursor, setAsideCursor] = useState(new Date(2026, 6, 1)); // 2026년 7월 const [asideCursor, setAsideCursor] = useState(new Date(2026, 6, 1)); // 2026년 7월
/* // 실 원천: 인증 workspaces → ScheduleEvent 매핑. 없으면 크롤링 10년치 폴백.
* : `GET /api/events` 10 (ExhibitionDto[]) event (V10 ). const usingReal = workspaces.length > 0;
* (NOT_IMPLEMENTED/NOT_FOUND)· (ALL_EVENTS) . const events: ScheduleEvent[] = useMemo(() => {
* ALL_EVENTS는 V10 seed_exhibitions_10yr (kintex.com ) shape· . if (!usingReal) return ALL_EVENTS;
*/ return workspaces.map((w, i) => ({
const query = useQuery<{ events: ScheduleEvent[]; degraded: boolean }>({ id: w.eventId,
queryKey: ['exhibitions'], title: w.eventName,
queryFn: async () => { // ★ category/estVisitors 는 WorkspaceDto 미포함 → 샘플/기본값
try { category: 'exhibition' as const,
const rows = await exhibitionApi.list(); status: (w.dday > 0 ? 'upcoming' : 'ongoing') as EventStatus,
return { events: rows.map(toScheduleEvent), degraded: false }; period: `${w.startDate} ${w.endDate}`,
} catch (err) { start: w.startDate,
if ( end: w.endDate,
err instanceof ApiRequestError && halls: w.hallLabel,
(err.code === 'NOT_IMPLEMENTED' || err.code === 'NOT_FOUND' || err.code === 'NETWORK') estVisitors: '집계 대기',
) { thumbIdx: i % THUMBS.length,
return { events: ALL_EVENTS, degraded: true }; }));
} }, [usingReal, workspaces]);
throw err;
}
},
});
const events = query.data?.events ?? [];
const degraded = query.data?.degraded ?? false;
const filtered = useMemo( const filtered = useMemo(
() => () =>
@ -142,8 +96,7 @@ export function ExhibitionSchedulePage() {
<div> <div>
<h1 className="kx-sched__title"> </h1> <h1 className="kx-sched__title"> </h1>
<p className="kx-sched__subtitle"> <p className="kx-sched__subtitle">
· {events.length.toLocaleString()} · 10 {ALL_EVENTS.length.toLocaleString()} (kintex.com )
{degraded && ' (kintex.com 크롤링 폴백)'}
</p> </p>
</div> </div>
</header> </header>
@ -173,7 +126,7 @@ export function ExhibitionSchedulePage() {
))} ))}
</select> </select>
<div className="kx-sched__filter-spacer" /> <div className="kx-sched__filter-spacer" />
{degraded && <span className="kx-bi__degraded"> API ( )</span>} {!usingReal && <span className="kx-bi__degraded"> API ( )</span>}
<div className="kx-seg" role="tablist" aria-label="보기 전환"> <div className="kx-seg" role="tablist" aria-label="보기 전환">
<button role="tab" aria-selected={view === 'list'} className={`kx-seg__btn ${view === 'list' ? 'is-active' : ''}`} onClick={() => setView('list')}></button> <button role="tab" aria-selected={view === 'list'} className={`kx-seg__btn ${view === 'list' ? 'is-active' : ''}`} onClick={() => setView('list')}></button>
<button role="tab" aria-selected={view === 'calendar'} className={`kx-seg__btn ${view === 'calendar' ? 'is-active' : ''}`} onClick={() => setView('calendar')}></button> <button role="tab" aria-selected={view === 'calendar'} className={`kx-seg__btn ${view === 'calendar' ? 'is-active' : ''}`} onClick={() => setView('calendar')}></button>
@ -181,28 +134,7 @@ export function ExhibitionSchedulePage() {
</div> </div>
<div className="kx-sched__split"> <div className="kx-sched__split">
{query.isLoading ? ( {view === 'list' ? (
<section className="kx-sched__list" aria-label="행사 목록 로딩 중" aria-busy="true">
{Array.from({ length: 5 }).map((_, i) => (
<article key={i} className="kx-evcard" aria-hidden="true">
<Skeleton height={112} width={160} radius={8} />
<div className="kx-evcard__body" style={{ flex: 1 }}>
<Skeleton height={20} width="60%" />
<Skeleton height={14} width="35%" />
<Skeleton height={14} width="80%" />
<Skeleton height={14} width="50%" />
</div>
</article>
))}
</section>
) : query.isError ? (
<section className="kx-sched__list" aria-label="행사 목록">
<ErrorState
message="전시 일정을 불러오지 못했습니다."
onRetry={() => query.refetch()}
/>
</section>
) : view === 'list' ? (
<section className="kx-sched__list" aria-label="행사 목록"> <section className="kx-sched__list" aria-label="행사 목록">
<p className="kx-sched__count tnum">{listed.length.toLocaleString()}</p> <p className="kx-sched__count tnum">{listed.length.toLocaleString()}</p>
{listed.length === 0 ? ( {listed.length === 0 ? (

View File

@ -101,10 +101,6 @@
font-weight: 700; font-weight: 700;
color: var(--color-neutral-900); color: var(--color-neutral-900);
} }
.kx-kpi__sub {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
}
.kx-kpi--warn { .kx-kpi--warn {
border-color: var(--color-violation-warn); border-color: var(--color-violation-warn);
} }

View File

@ -1,324 +0,0 @@
/*
* SCR-45 (meeting). 참조: UIWS meeting/MeetingDetailPage.
* : 회의 / : 상세(· ·).
* : /api/work/meetings (get·create·minutes·actions·action status).
* : 녹음 ·STT ·AI ·Jasper PDF UI는 disabled+. 07_work_api_gaps.md .
*/
import { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { meetingApi } from '../../api/endpoints';
import type { MeetingSaveRequest } from '../../api/types';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconDownload, IconPlus } from '../../components/ui/icons';
import { StatusPill, errMessage, fmtDateTime, useToast } from './workShared';
import './work.css';
const NOT_SUPPORTED = '백엔드 미지원 — STT/PDF 파이프라인 연동 후 활성화';
export function MeetingPage() {
const qc = useQueryClient();
const { show, node: toast } = useToast();
const [selectedId, setSelectedId] = useState<string | null>(null);
const [composing, setComposing] = useState(false);
const [minutesDraft, setMinutesDraft] = useState('');
const [actionText, setActionText] = useState('');
const listQ = useQuery({
queryKey: ['meetings'],
queryFn: () => meetingApi.list({ page: 0, size: 50 }),
});
const detailQ = useQuery({
queryKey: ['meeting', selectedId],
queryFn: () => meetingApi.get(selectedId as string),
enabled: !!selectedId,
});
useEffect(() => {
if (detailQ.data) setMinutesDraft(detailQ.data.meeting.minutes ?? '');
}, [detailQ.data]);
const createM = useMutation({
mutationFn: (b: MeetingSaveRequest) => meetingApi.create(b),
onSuccess: (m) => {
show('회의가 등록되었습니다.');
setComposing(false);
setSelectedId(m.id);
qc.invalidateQueries({ queryKey: ['meetings'] });
},
onError: (e) => show(errMessage(e)),
});
const minutesM = useMutation({
mutationFn: ({ id, minutes }: { id: string; minutes: string }) =>
meetingApi.saveMinutes(id, minutes),
onSuccess: () => {
show('회의록이 저장되었습니다.');
qc.invalidateQueries({ queryKey: ['meeting', selectedId] });
},
onError: (e) => show(errMessage(e)),
});
const actionM = useMutation({
mutationFn: ({ id, item }: { id: string; item: string }) =>
meetingApi.addAction(id, { actionItem: item }),
onSuccess: () => {
setActionText('');
qc.invalidateQueries({ queryKey: ['meeting', selectedId] });
},
onError: (e) => show(errMessage(e)),
});
const actionStatusM = useMutation({
mutationFn: ({ actionId, value }: { actionId: string; value: string }) =>
meetingApi.actionStatus(actionId, value),
onSuccess: () => qc.invalidateQueries({ queryKey: ['meeting', selectedId] }),
onError: (e) => show(errMessage(e)),
});
const rows = listQ.data?.items ?? [];
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle">(D-30) · </p>
</div>
<Button leadingIcon={<IconPlus size={16} />} onClick={() => setComposing(true)}>
</Button>
</header>
<div className="kx-meet">
{/* 목록 */}
<section className="kx-card" aria-label="회의 목록" style={{ padding: 0 }}>
<div className="kx-scroll-70">
{listQ.isLoading &&
Array.from({ length: 5 }).map((_, i) => (
<div className="kx-meet__list-item" key={i}>
<Skeleton height={16} width="70%" />
<Skeleton height={13} width="40%" />
</div>
))}
{listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
{!listQ.isLoading && !listQ.isError && rows.length === 0 && (
<EmptyState title="등록된 회의가 없습니다" />
)}
{rows.map((m) => (
<div
key={m.id}
className={`kx-meet__list-item ${selectedId === m.id ? 'is-selected' : ''}`}
onClick={() => {
setComposing(false);
setSelectedId(m.id);
}}
>
<span className="kx-list-table__title">{m.title}</span>
<span className="kx-detail__meta">
{fmtDateTime(m.meetingAt)}
{m.location && ` · ${m.location}`}
</span>
</div>
))}
</div>
</section>
{/* 상세 */}
<section className="kx-detail" aria-label="회의 상세" style={{ position: 'static' }}>
{composing ? (
<MeetingCompose
saving={createM.isPending}
onCancel={() => setComposing(false)}
onSave={(b) => createM.mutate(b)}
/>
) : !selectedId ? (
<EmptyState title="회의를 선택하세요" description="목록에서 회의를 선택하면 상세가 표시됩니다." />
) : detailQ.isLoading ? (
<>
<Skeleton height={24} width="60%" />
<Skeleton height={120} />
</>
) : detailQ.isError ? (
<ErrorState message={errMessage(detailQ.error)} onRetry={() => detailQ.refetch()} />
) : detailQ.data ? (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title">{detailQ.data.meeting.title}</h2>
<span title={NOT_SUPPORTED} style={{ display: 'inline-flex' }}>
<Button variant="secondary" disabled leadingIcon={<IconDownload size={16} />}>
PDF
</Button>
</span>
</div>
<div className="kx-detail__meta">
<span>{detailQ.data.meeting.organizerName}</span>
<span>· {fmtDateTime(detailQ.data.meeting.meetingAt)}</span>
{detailQ.data.meeting.location && <span>· {detailQ.data.meeting.location}</span>}
</div>
{/* 녹음/STT (미지원) */}
<div className="kx-meet__player" title={NOT_SUPPORTED}>
· STT ( )
</div>
{detailQ.data.meeting.content && (
<div className="kx-field">
<span className="kx-label"> </span>
<div className="kx-detail__body">{detailQ.data.meeting.content}</div>
</div>
)}
{/* 회의록 편집 */}
<div className="kx-field">
<span className="kx-label"></span>
<textarea
className="kx-textarea"
style={{ minHeight: 140 }}
value={minutesDraft}
onChange={(e) => setMinutesDraft(e.target.value)}
placeholder="회의 내용을 기록하세요."
/>
<div style={{ marginTop: 8 }}>
<Button
disabled={minutesM.isPending}
onClick={() => minutesM.mutate({ id: detailQ.data!.meeting.id, minutes: minutesDraft })}
>
{minutesM.isPending ? '저장 중…' : '회의록 저장'}
</Button>
</div>
</div>
{/* 액션아이템 */}
<div className="kx-field">
<span className="kx-label"></span>
<table className="kx-list-table">
<thead>
<tr>
<th style={{ width: 40 }}>#</th>
<th></th>
<th style={{ width: 96 }}></th>
</tr>
</thead>
<tbody>
{detailQ.data.actions.length === 0 && (
<tr>
<td colSpan={3} className="kx-list-table__muted">
.
</td>
</tr>
)}
{detailQ.data.actions.map((a) => (
<tr key={a.id} style={{ cursor: 'default' }}>
<td className="kx-num">{a.seq}</td>
<td>{a.actionItem}</td>
<td>
<button
className="kx-pill kx-pill--neutral"
style={{ cursor: 'pointer', border: 'none' }}
onClick={() =>
actionStatusM.mutate({
actionId: a.id,
value: a.status === 'DONE' ? 'OPEN' : 'DONE',
})
}
title="상태 토글"
>
<StatusPill value={a.status} />
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="kx-thread__compose" style={{ marginTop: 8 }}>
<input
className="kx-input"
placeholder="새 액션아이템…"
value={actionText}
onChange={(e) => setActionText(e.target.value)}
onKeyDown={(e) =>
e.key === 'Enter' &&
actionText.trim() &&
actionM.mutate({ id: detailQ.data!.meeting.id, item: actionText })
}
/>
<Button
disabled={!actionText.trim() || actionM.isPending}
onClick={() => actionM.mutate({ id: detailQ.data!.meeting.id, item: actionText })}
>
</Button>
</div>
</div>
</>
) : null}
</section>
</div>
{toast}
</div>
);
}
function MeetingCompose({
onCancel,
onSave,
saving,
}: {
onCancel: () => void;
onSave: (b: MeetingSaveRequest) => void;
saving: boolean;
}) {
const now = new Date();
const [form, setForm] = useState<MeetingSaveRequest>({
title: '',
location: '',
meetingAt: `${now.toISOString().slice(0, 16)}`,
content: '',
});
return (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title"> </h2>
</div>
<label className="kx-field">
<span className="kx-label"> *</span>
<input className="kx-input" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
</label>
<div className="kx-formgrid">
<label className="kx-field">
<span className="kx-label"> *</span>
<input
type="datetime-local"
className="kx-input"
value={form.meetingAt}
onChange={(e) => setForm({ ...form, meetingAt: e.target.value })}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<input
className="kx-input"
value={form.location ?? ''}
onChange={(e) => setForm({ ...form, location: e.target.value })}
/>
</label>
</div>
<label className="kx-field">
<span className="kx-label"></span>
<textarea
className="kx-textarea"
value={form.content ?? ''}
onChange={(e) => setForm({ ...form, content: e.target.value })}
/>
</label>
<div className="kx-detail__actions">
<Button disabled={!form.title.trim() || saving} onClick={() => onSave(form)}>
{saving ? '등록 중…' : '등록'}
</Button>
<Button variant="ghost" onClick={onCancel}>
</Button>
</div>
</>
);
}

View File

@ -1,258 +0,0 @@
/*
* SCR-41 (message). 참조: UIWS message/*.
* 3-패널: 폴더 (/) · · /. · .
* : /api/work/messages (inbox·sent·send·read·delete).
* : 부서 ID . 07_work_api_gaps.md .
*/
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { messageApi } from '../../api/endpoints';
import type { MessageDto, MessageSendRequest } from '../../api/types';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconClose, IconPlus } from '../../components/ui/icons';
import { errMessage, fmtDateTime, timeAgo, useToast } from './workShared';
import './work.css';
type Folder = 'inbox' | 'sent';
export function MessagePage() {
const qc = useQueryClient();
const { show, node: toast } = useToast();
const [folder, setFolder] = useState<Folder>('inbox');
const [selected, setSelected] = useState<MessageDto | null>(null);
const [composing, setComposing] = useState(false);
const unreadQ = useQuery({ queryKey: ['msg-unread'], queryFn: () => messageApi.unreadCount() });
const listQ = useQuery({
queryKey: ['messages', folder],
queryFn: () => (folder === 'inbox' ? messageApi.inbox(0, 50) : messageApi.sent(0, 50)),
});
const readM = useMutation({
mutationFn: (id: string) => messageApi.read(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['messages', 'inbox'] });
qc.invalidateQueries({ queryKey: ['msg-unread'] });
},
});
const delM = useMutation({
mutationFn: (id: string) => messageApi.remove(id),
onSuccess: () => {
show('삭제되었습니다.');
setSelected(null);
qc.invalidateQueries({ queryKey: ['messages'] });
qc.invalidateQueries({ queryKey: ['msg-unread'] });
},
onError: (e) => show(errMessage(e)),
});
const sendM = useMutation({
mutationFn: (body: MessageSendRequest) => messageApi.send(body),
onSuccess: () => {
show('쪽지를 보냈습니다.');
setComposing(false);
qc.invalidateQueries({ queryKey: ['messages', 'sent'] });
},
onError: (e) => show(errMessage(e)),
});
function openMessage(m: MessageDto) {
setSelected(m);
if (folder === 'inbox' && !m.readAt) readM.mutate(m.id);
}
const rows = listQ.data?.items ?? [];
const unread = unreadQ.data?.unread ?? 0;
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle"> · </p>
</div>
<Button leadingIcon={<IconPlus size={16} />} onClick={() => setComposing(true)}>
</Button>
</header>
<div className="kx-msg">
{/* 폴더 레일 */}
<nav className="kx-msg__rail" aria-label="쪽지 폴더">
<button
className={`kx-msg__rail-btn ${folder === 'inbox' ? 'is-active' : ''}`}
onClick={() => {
setFolder('inbox');
setSelected(null);
}}
>
{unread > 0 && <span className="kx-msg__rail-count tnum">{unread}</span>}
</button>
<button
className={`kx-msg__rail-btn ${folder === 'sent' ? 'is-active' : ''}`}
onClick={() => {
setFolder('sent');
setSelected(null);
}}
>
</button>
</nav>
{/* 목록 */}
<section className="kx-msg__list" aria-label={folder === 'inbox' ? '받은 쪽지 목록' : '보낸 쪽지 목록'}>
{listQ.isLoading &&
Array.from({ length: 6 }).map((_, i) => (
<div className="kx-msg__item" key={i}>
<Skeleton height={16} width="60%" />
<Skeleton height={14} width="90%" />
</div>
))}
{listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
{!listQ.isLoading && !listQ.isError && rows.length === 0 && (
<EmptyState
title={folder === 'inbox' ? '받은 쪽지가 없습니다' : '보낸 쪽지가 없습니다'}
/>
)}
{rows.map((m) => {
const unreadItem = folder === 'inbox' && !m.readAt;
return (
<div
key={m.id}
className={`kx-msg__item ${selected?.id === m.id ? 'is-selected' : ''} ${
unreadItem ? 'is-unread' : ''
}`}
onClick={() => openMessage(m)}
>
<div className="kx-msg__item-top">
<span className="kx-msg__item-sender">
{unreadItem && <span className="kx-msg__unread-dot" aria-label="안읽음" />}{' '}
{folder === 'inbox' ? m.senderName : m.title || '(제목 없음)'}
</span>
<span className="kx-msg__item-time">{timeAgo(m.createdAt)}</span>
</div>
<span className="kx-msg__item-title">{m.title || '(제목 없음)'}</span>
</div>
);
})}
</section>
{/* 읽기/작성 */}
<section className="kx-msg__read" aria-label="쪽지 내용">
{composing ? (
<ComposeForm
onCancel={() => setComposing(false)}
onSend={(b) => sendM.mutate(b)}
sending={sendM.isPending}
/>
) : selected ? (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title">{selected.title || '(제목 없음)'}</h2>
<Button
variant="danger"
onClick={() => {
if (window.confirm('이 쪽지를 삭제할까요?')) delM.mutate(selected.id);
}}
>
</Button>
</div>
<div className="kx-detail__meta">
<span>: {selected.senderName}</span>
<span>· {fmtDateTime(selected.createdAt)}</span>
{selected.recvType && <span>· {selected.recvType === 'REF' ? '참조' : '수신'}</span>}
</div>
<div className="kx-detail__body">{selected.content || '(내용 없음)'}</div>
</>
) : (
<EmptyState title="쪽지를 선택하세요" description="목록에서 쪽지를 선택하면 내용이 표시됩니다." />
)}
</section>
</div>
{toast}
</div>
);
}
function ComposeForm({
onCancel,
onSend,
sending,
}: {
onCancel: () => void;
onSend: (b: MessageSendRequest) => void;
sending: boolean;
}) {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [recipients, setRecipients] = useState<string[]>([]);
const [entry, setEntry] = useState('');
function addRecipient() {
const v = entry.trim();
if (v && !recipients.includes(v)) setRecipients([...recipients, v]);
setEntry('');
}
return (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title"> </h2>
</div>
<div className="kx-field">
<span className="kx-label"> ( ID)</span>
<div className="kx-tags">
{recipients.map((r) => (
<span className="kx-tag" key={r}>
{r}
<button
onClick={() => setRecipients(recipients.filter((x) => x !== r))}
aria-label={`${r} 제거`}
>
<IconClose size={12} />
</button>
</span>
))}
<input
value={entry}
placeholder="ID 입력 후 Enter"
onChange={(e) => setEntry(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addRecipient();
}
}}
/>
</div>
</div>
<label className="kx-field">
<span className="kx-label"></span>
<input className="kx-input" value={title} onChange={(e) => setTitle(e.target.value)} />
</label>
<label className="kx-field">
<span className="kx-label"></span>
<textarea
className="kx-textarea"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</label>
<div className="kx-detail__actions">
<Button
disabled={recipients.length === 0 || sending}
onClick={() => onSend({ title, content, recipientIds: recipients })}
>
{sending ? '보내는 중…' : '보내기'}
</Button>
<Button variant="ghost" onClick={onCancel}>
</Button>
</div>
</>
);
}

View File

@ -1,243 +0,0 @@
/*
* SCR-48 ·. 참조: UIWS auth/MyProfilePage.
* : (···/) / : 설정 .
* : /api/auth/me·/api/auth/otp/status. 2FA /otp-setup ( ).
* : 프로필 · · / . 07_work_api_gaps.md .
*/
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { authApi } from '../../api/endpoints';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { RoleBadge } from '../../components/ui/Badge';
import { ErrorState, Skeleton } from '../../components/ui/States';
import { IconCheckCircle } from '../../components/ui/icons';
import { useToast } from './workShared';
import './work.css';
type Tab = 'profile' | 'security' | 'notify' | 'theme';
const TABS: { v: Tab; l: string }[] = [
{ v: 'profile', l: '프로필' },
{ v: 'security', l: '보안(2차 인증)' },
{ v: 'notify', l: '알림 설정' },
{ v: 'theme', l: '테마·언어' },
];
const PREF_KEY = 'kintex.prefs';
interface Prefs {
theme: 'light' | 'dark';
lang: 'ko' | 'en' | 'zh' | 'ja';
notifyDeadline: boolean;
notifyApproval: boolean;
notifyPayment: boolean;
}
const DEFAULT_PREFS: Prefs = {
theme: 'light',
lang: 'ko',
notifyDeadline: true,
notifyApproval: true,
notifyPayment: true,
};
function loadPrefs(): Prefs {
try {
return { ...DEFAULT_PREFS, ...JSON.parse(localStorage.getItem(PREF_KEY) ?? '{}') };
} catch {
return DEFAULT_PREFS;
}
}
export function MyPage() {
const { show, node: toast } = useToast();
const storeUser = useAuthStore((s) => s.user);
const workspaces = useAuthStore((s) => s.workspaces);
const [tab, setTab] = useState<Tab>('profile');
const [prefs, setPrefs] = useState<Prefs>(loadPrefs);
const meQ = useQuery({ queryKey: ['me'], queryFn: () => authApi.me() });
const otpQ = useQuery({ queryKey: ['otp-status'], queryFn: () => authApi.otpStatus() });
useEffect(() => {
document.documentElement.setAttribute('data-theme', prefs.theme);
document.documentElement.setAttribute('lang', prefs.lang);
}, [prefs.theme, prefs.lang]);
function savePrefs(next: Prefs) {
setPrefs(next);
localStorage.setItem(PREF_KEY, JSON.stringify(next));
show('설정이 저장되었습니다.');
}
const displayName = meQ.data?.displayName ?? storeUser?.displayName ?? '사용자';
const roles = meQ.data?.eventRoles ? Object.values(meQ.data.eventRoles) : [];
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle"> · · · </p>
</div>
</header>
<div className="kx-my">
<nav className="kx-my__rail" aria-label="설정 탭">
{TABS.map((t) => (
<button
key={t.v}
className={`kx-my__rail-btn ${tab === t.v ? 'is-active' : ''}`}
onClick={() => setTab(t.v)}
>
{t.l}
</button>
))}
</nav>
<section className="kx-card" aria-label="설정 내용">
{/* 프로필 */}
{tab === 'profile' && (
<>
<div className="kx-card__head">
<h2></h2>
</div>
{meQ.isLoading ? (
<Skeleton height={80} />
) : meQ.isError ? (
<ErrorState onRetry={() => meQ.refetch()} />
) : (
<div style={{ display: 'flex', gap: 20, alignItems: 'center', flexWrap: 'wrap' }}>
<span className="kx-my__avatar">{displayName[0] ?? '·'}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<strong style={{ fontSize: 18 }}>{displayName}</strong>
<span className="kx-list-table__muted"> ID: {meQ.data?.userId}</span>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{meQ.data?.hallManager && <span className="kx-pill kx-pill--info"></span>}
{roles.map((r, i) => (
<RoleBadge key={i} role={r} />
))}
</div>
<span className="kx-list-table__muted">
{workspaces.length}
</span>
</div>
</div>
)}
<p className="kx-list-table__muted" style={{ marginTop: 12, fontSize: 12 }}>
.
</p>
</>
)}
{/* 보안 (2FA) */}
{tab === 'security' && (
<>
<div className="kx-card__head">
<h2>2 (OTP)</h2>
</div>
{otpQ.isLoading ? (
<Skeleton height={60} />
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{otpQ.data?.otpEnabled ? (
<>
<span style={{ color: 'var(--color-success)', display: 'inline-flex' }}>
<IconCheckCircle size={20} />
</span>
<span>2 .</span>
</>
) : (
<span className="kx-pill kx-pill--warn">2 </span>
)}
</div>
<p className="kx-list-table__muted" style={{ fontSize: 13 }}>
Google Authenticator TOTP 2 .
· .
</p>
<div>
<Link to="/otp-setup">
<Button>{otpQ.data?.otpEnabled ? '2차 인증 재설정' : '2차 인증 등록'}</Button>
</Link>
</div>
</div>
)}
</>
)}
{/* 알림 설정 */}
{tab === 'notify' && (
<>
<div className="kx-card__head">
<h2> </h2>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{(
[
['notifyDeadline', '마감 D-데이 알림'],
['notifyApproval', '승인·검수 알림'],
['notifyPayment', '결제·정산 알림'],
] as const
).map(([key, label]) => (
<label className="kx-switch" key={key}>
<input
type="checkbox"
checked={prefs[key]}
onChange={(e) => savePrefs({ ...prefs, [key]: e.target.checked })}
/>
<span>{label}</span>
</label>
))}
<p className="kx-list-table__muted" style={{ fontSize: 12 }}>
. .
</p>
</div>
</>
)}
{/* 테마·언어 */}
{tab === 'theme' && (
<>
<div className="kx-card__head">
<h2>·</h2>
</div>
<div className="kx-formgrid">
<label className="kx-field">
<span className="kx-label"></span>
<div className="kx-seg" role="tablist">
{(['light', 'dark'] as const).map((t) => (
<button
key={t}
className={`kx-seg__btn ${prefs.theme === t ? 'is-active' : ''}`}
onClick={() => savePrefs({ ...prefs, theme: t })}
>
{t === 'light' ? '라이트' : '다크'}
</button>
))}
</div>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<select
className="kx-select"
value={prefs.lang}
onChange={(e) => savePrefs({ ...prefs, lang: e.target.value as Prefs['lang'] })}
>
<option value="ko"></option>
<option value="en">English</option>
<option value="zh"></option>
<option value="ja"></option>
</select>
</label>
</div>
<p className="kx-list-table__muted" style={{ marginTop: 12, fontSize: 12 }}>
· . .
</p>
</>
)}
</section>
</div>
{toast}
</div>
);
}

View File

@ -1,255 +0,0 @@
/*
* SCR-42 (notice). 참조: UIWS notice/*.
* () + / + . =( requireManager) UI .
* : /api/work/notices.
*/
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { noticeApi } from '../../api/endpoints';
import type { NoticeDto, NoticeSaveRequest } from '../../api/types';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconPlus, IconSearch } from '../../components/ui/icons';
import { errMessage, fmtDateTime, useToast } from './workShared';
import './work.css';
export function NoticePage() {
const qc = useQueryClient();
const { show, node: toast } = useToast();
const isManager = useAuthStore((s) => s.user?.hallManager ?? false);
const [keyword, setKeyword] = useState('');
const [term, setTerm] = useState('');
const [selectedId, setSelectedId] = useState<string | null>(null);
const [composing, setComposing] = useState(false);
const listQ = useQuery({
queryKey: ['notices', term],
queryFn: () => noticeApi.list({ keyword: term || undefined, page: 0, size: 50 }),
});
const detailQ = useQuery({
queryKey: ['notice', selectedId],
queryFn: () => noticeApi.get(selectedId as string),
enabled: !!selectedId && !composing,
});
const saveM = useMutation({
mutationFn: (body: NoticeSaveRequest) => noticeApi.create(body),
onSuccess: (n: NoticeDto) => {
show('공지가 등록되었습니다.');
setComposing(false);
setSelectedId(n.id);
qc.invalidateQueries({ queryKey: ['notices'] });
},
onError: (e) => show(errMessage(e)),
});
const delM = useMutation({
mutationFn: (id: string) => noticeApi.remove(id),
onSuccess: () => {
show('삭제되었습니다.');
setSelectedId(null);
qc.invalidateQueries({ queryKey: ['notices'] });
},
onError: (e) => show(errMessage(e)),
});
const rows = listQ.data?.items ?? [];
const pinned = rows.filter((r) => r.pinned);
const normal = rows.filter((r) => !r.pinned);
const renderRow = (r: NoticeDto) => (
<tr
key={r.id}
className={selectedId === r.id ? 'is-selected' : ''}
onClick={() => {
setComposing(false);
setSelectedId(r.id);
}}
>
<td>
{r.pinned && <span className="kx-pill kx-pill--danger" style={{ marginRight: 6 }}></span>}
<span className="kx-list-table__title">{r.title}</span>
{r.category && <span className="kx-list-table__muted"> · {r.category}</span>}
</td>
<td className="kx-list-table__muted">{r.authorName}</td>
<td className="kx-list-table__muted kx-num">{fmtDateTime(r.publishedAt ?? r.createdAt)}</td>
<td className="kx-num">{r.viewCount ?? 0}</td>
</tr>
);
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle">· · </p>
</div>
{isManager && (
<Button leadingIcon={<IconPlus size={16} />} onClick={() => setComposing(true)}>
</Button>
)}
</header>
<div className="kx-split">
<section className="kx-card" aria-label="공지 목록">
<div className="kx-filterbar">
<div className="kx-search__box" style={{ height: 40, flex: 1 }}>
<IconSearch size={18} />
<input
placeholder="공지 검색…"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && setTerm(keyword)}
/>
</div>
<Button variant="secondary" onClick={() => setTerm(keyword)}>
</Button>
</div>
<div className="kx-table-scroll kx-scroll-70">
<table className="kx-list-table">
<thead>
<tr>
<th></th>
<th style={{ width: 110 }}></th>
<th style={{ width: 140 }}></th>
<th style={{ width: 70 }} className="kx-num">
</th>
</tr>
</thead>
<tbody>
{listQ.isLoading &&
Array.from({ length: 6 }).map((_, i) => (
<tr key={i}>
<td colSpan={4}>
<Skeleton height={20} />
</td>
</tr>
))}
{!listQ.isLoading && pinned.map(renderRow)}
{!listQ.isLoading && normal.map(renderRow)}
</tbody>
</table>
</div>
{listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
{!listQ.isLoading && !listQ.isError && rows.length === 0 && (
<EmptyState title="등록된 공지가 없습니다" />
)}
</section>
<aside className="kx-detail" aria-label="공지 상세">
{composing ? (
<NoticeCompose
saving={saveM.isPending}
onCancel={() => setComposing(false)}
onSave={(b) => saveM.mutate(b)}
/>
) : selectedId ? (
detailQ.isLoading ? (
<>
<Skeleton height={24} width="70%" />
<Skeleton height={16} />
<Skeleton height={120} />
</>
) : detailQ.isError ? (
<ErrorState message={errMessage(detailQ.error)} onRetry={() => detailQ.refetch()} />
) : detailQ.data ? (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title">
{detailQ.data.pinned && (
<span className="kx-pill kx-pill--danger" style={{ marginRight: 6 }}>
</span>
)}
{detailQ.data.title}
</h2>
{isManager && (
<Button
variant="danger"
onClick={() => {
if (window.confirm('이 공지를 삭제할까요?')) delM.mutate(detailQ.data!.id);
}}
>
</Button>
)}
</div>
<div className="kx-detail__meta">
<span>{detailQ.data.authorName}</span>
<span>· {fmtDateTime(detailQ.data.publishedAt ?? detailQ.data.createdAt)}</span>
<span>· {detailQ.data.viewCount ?? 0}</span>
</div>
<div className="kx-detail__body">{detailQ.data.content || '(내용 없음)'}</div>
</>
) : null
) : (
<EmptyState title="공지를 선택하세요" description="목록에서 공지를 선택하면 내용이 표시됩니다." />
)}
</aside>
</div>
{toast}
</div>
);
}
function NoticeCompose({
onCancel,
onSave,
saving,
}: {
onCancel: () => void;
onSave: (b: NoticeSaveRequest) => void;
saving: boolean;
}) {
const [title, setTitle] = useState('');
const [category, setCategory] = useState('일반');
const [content, setContent] = useState('');
const [pinned, setPinned] = useState(false);
return (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title"> </h2>
</div>
<label className="kx-field">
<span className="kx-label"> *</span>
<input className="kx-input" value={title} onChange={(e) => setTitle(e.target.value)} />
</label>
<label className="kx-field">
<span className="kx-label"></span>
<select className="kx-select" value={category} onChange={(e) => setCategory(e.target.value)}>
{['일반', '행사', '규정', '시스템'].map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<textarea className="kx-textarea" value={content} onChange={(e) => setContent(e.target.value)} />
</label>
<label className="kx-switch">
<input type="checkbox" checked={pinned} onChange={(e) => setPinned(e.target.checked)} />
<span> </span>
</label>
<div className="kx-detail__actions">
<Button
disabled={!title.trim() || saving}
onClick={() => onSave({ title, category, content, pinned })}
>
{saving ? '등록 중…' : '등록'}
</Button>
<Button variant="ghost" onClick={onCancel}>
</Button>
</div>
</>
);
}

View File

@ -1,171 +0,0 @@
/*
* SCR-47 (notification). 참조: UIWS NotificationsPage.
* + + / + + .
* : /api/work/notifications (list·unread-count·read·read-all·delete).
*/
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { notificationApi } from '../../api/endpoints';
import type { NotificationDto } from '../../api/types';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconArrowRight, IconBell, IconCheck } from '../../components/ui/icons';
import { errMessage, timeAgo, useToast } from './workShared';
import './work.css';
// 알림 유형 라벨(백엔드 notiType 원문 매핑, 미지정은 원문 표기).
const TYPE_LABEL: Record<string, string> = {
DEADLINE: '마감',
AWARD: '낙찰',
APPROVAL: '승인',
PAYMENT: '결제',
SYSTEM: '시스템',
};
export function NotificationCenterPage() {
const qc = useQueryClient();
const navigate = useNavigate();
const { show, node: toast } = useToast();
const [category, setCategory] = useState<string>('ALL');
const listQ = useQuery({
queryKey: ['notifications-center'],
queryFn: () => notificationApi.list({ page: 0, size: 100 }),
});
const readM = useMutation({
mutationFn: (id: string) => notificationApi.read(id),
onSuccess: () => invalidate(),
});
const readAllM = useMutation({
mutationFn: () => notificationApi.readAll(),
onSuccess: () => {
show('모두 읽음 처리했습니다.');
invalidate();
},
onError: (e) => show(errMessage(e)),
});
const delM = useMutation({
mutationFn: (id: string) => notificationApi.remove(id),
onSuccess: () => invalidate(),
onError: (e) => show(errMessage(e)),
});
function invalidate() {
qc.invalidateQueries({ queryKey: ['notifications-center'] });
qc.invalidateQueries({ queryKey: ['notif-unread'] });
}
const items = listQ.data?.items ?? [];
const categories = useMemo(() => {
const present = Array.from(new Set(items.map((n) => n.notiType)));
return ['ALL', ...present];
}, [items]);
const filtered = category === 'ALL' ? items : items.filter((n) => n.notiType === category);
const unreadCount = items.filter((n) => !n.readAt).length;
function openItem(n: NotificationDto) {
if (!n.readAt) readM.mutate(n.id);
if (n.link) {
if (/^https?:\/\//.test(n.link)) window.open(n.link, '_blank', 'noopener');
else navigate(n.link);
}
}
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle">
···· {unreadCount > 0 && `· 안읽음 ${unreadCount}`}
</p>
</div>
<div className="kx-work__head-actions">
<Button
variant="secondary"
leadingIcon={<IconCheck size={16} />}
disabled={unreadCount === 0 || readAllM.isPending}
onClick={() => readAllM.mutate()}
>
</Button>
</div>
</header>
<div className="kx-noti__chips">
{categories.map((c) => (
<button
key={c}
className={`kx-chip ${category === c ? 'is-active' : ''}`}
onClick={() => setCategory(c)}
>
{c === 'ALL' ? '전체' : (TYPE_LABEL[c] ?? c)}
</button>
))}
</div>
<section className="kx-card" style={{ padding: 0 }} aria-label="알림 목록">
{listQ.isLoading &&
Array.from({ length: 6 }).map((_, i) => (
<div className="kx-noti__item" key={i}>
<Skeleton height={32} width={32} radius={16} />
<div style={{ flex: 1 }}>
<Skeleton height={16} width="40%" />
<Skeleton height={14} width="70%" />
</div>
</div>
))}
{listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
{!listQ.isLoading && !listQ.isError && filtered.length === 0 && (
<EmptyState title="새 알림이 없습니다" icon={<IconBell size={28} />} />
)}
{filtered.map((n) => (
<div key={n.id} className={`kx-noti__item ${!n.readAt ? 'is-unread' : ''}`}>
<span className="kx-noti__icon">
<IconBell size={16} />
</span>
<div
className="kx-noti__body"
style={{ cursor: n.link ? 'pointer' : 'default' }}
onClick={() => openItem(n)}
>
<div className="kx-noti__title">
<span className="kx-pill kx-pill--info" style={{ marginRight: 6 }}>
{TYPE_LABEL[n.notiType] ?? n.notiType}
</span>
{n.title}
</div>
{n.message && <div className="kx-noti__desc">{n.message}</div>}
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
<span className="kx-noti__time">{timeAgo(n.createdAt)}</span>
<div style={{ display: 'flex', gap: 6 }}>
{n.link && (
<button
className="kx-tag"
onClick={() => openItem(n)}
aria-label="바로가기"
>
<IconArrowRight size={12} />
</button>
)}
<button className="kx-tag" onClick={() => delM.mutate(n.id)} aria-label="삭제">
</button>
</div>
</div>
{!n.readAt && <span className="kx-noti__dot" aria-label="안읽음" />}
</div>
))}
</section>
<p className="kx-list-table__muted" style={{ fontSize: 12 }}>
&gt; .
</p>
</div>
);
}

View File

@ -1,284 +0,0 @@
/*
* SCR-43 (opinion). 참조: UIWS opinion/*.
* : 접수 + ( ) / : 상세( ).
* : /api/work/opinions. ·=(requireManager).
*/
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { opinionApi } from '../../api/endpoints';
import type { OpinionDto, OpinionSaveRequest } from '../../api/types';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { StatusPill, errMessage, fmtDateTime, useToast } from './workShared';
import './work.css';
const CATEGORIES = ['규정 문의', '개선 제안', '불편 신고', '기타'];
const STATUS_FLOW = ['접수', '검토중', '답변완료'];
export function OpinionPage() {
const qc = useQueryClient();
const { show, node: toast } = useToast();
const isManager = useAuthStore((s) => s.user?.hallManager ?? false);
const [statusFilter, setStatusFilter] = useState('');
const [selectedId, setSelectedId] = useState<string | null>(null);
const [form, setForm] = useState<OpinionSaveRequest>({
category: CATEGORIES[0],
title: '',
content: '',
secretYn: 'N',
});
const [comment, setComment] = useState('');
const listQ = useQuery({
queryKey: ['opinions', statusFilter],
queryFn: () => opinionApi.list({ status: statusFilter || undefined, page: 0, size: 50 }),
});
const detailQ = useQuery({
queryKey: ['opinion', selectedId],
queryFn: () => opinionApi.get(selectedId as string),
enabled: !!selectedId,
});
const createM = useMutation({
mutationFn: (b: OpinionSaveRequest) => opinionApi.create(b),
onSuccess: () => {
show('의견이 접수되었습니다.');
setForm({ category: CATEGORIES[0], title: '', content: '', secretYn: 'N' });
qc.invalidateQueries({ queryKey: ['opinions'] });
},
onError: (e) => show(errMessage(e)),
});
const statusM = useMutation({
mutationFn: ({ id, value }: { id: string; value: string }) => opinionApi.changeStatus(id, value),
onSuccess: () => {
show('상태가 변경되었습니다.');
qc.invalidateQueries({ queryKey: ['opinions'] });
qc.invalidateQueries({ queryKey: ['opinion', selectedId] });
},
onError: (e) => show(errMessage(e)),
});
const commentM = useMutation({
mutationFn: ({ id, content }: { id: string; content: string }) => opinionApi.comment(id, content),
onSuccess: () => {
show('답변이 등록되었습니다.');
setComment('');
qc.invalidateQueries({ queryKey: ['opinion', selectedId] });
},
onError: (e) => show(errMessage(e)),
});
const rows = listQ.data?.items ?? [];
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle"> · · </p>
</div>
</header>
<div className="kx-split">
{/* 좌: 접수 폼 + 목록 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<section className="kx-card" aria-label="의견 접수">
<div className="kx-card__head">
<h2> </h2>
</div>
<div className="kx-formgrid">
<label className="kx-field">
<span className="kx-label"></span>
<select
className="kx-select"
value={form.category ?? ''}
onChange={(e) => setForm({ ...form, category: e.target.value })}
>
{CATEGORIES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</label>
<label className="kx-switch" style={{ alignSelf: 'end' }}>
<input
type="checkbox"
checked={form.secretYn === 'Y'}
onChange={(e) => setForm({ ...form, secretYn: e.target.checked ? 'Y' : 'N' })}
/>
<span>(·)</span>
</label>
<label className="kx-field kx-field--full">
<span className="kx-label"> *</span>
<input
className="kx-input"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
/>
</label>
<label className="kx-field kx-field--full">
<span className="kx-label"></span>
<textarea
className="kx-textarea"
value={form.content ?? ''}
onChange={(e) => setForm({ ...form, content: e.target.value })}
/>
</label>
</div>
<div className="kx-detail__actions" style={{ marginTop: 12 }}>
<Button disabled={!form.title.trim() || createM.isPending} onClick={() => createM.mutate(form)}>
{createM.isPending ? '제출 중…' : '제출'}
</Button>
</div>
</section>
<section className="kx-card" aria-label="접수 목록">
<div className="kx-card__head">
<h2> </h2>
<select
className="kx-select"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value=""> </option>
{STATUS_FLOW.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</div>
<div className="kx-table-scroll">
<table className="kx-list-table">
<thead>
<tr>
<th></th>
<th style={{ width: 96 }}></th>
<th style={{ width: 96 }}></th>
<th style={{ width: 120 }}></th>
</tr>
</thead>
<tbody>
{listQ.isLoading &&
Array.from({ length: 4 }).map((_, i) => (
<tr key={i}>
<td colSpan={4}>
<Skeleton height={20} />
</td>
</tr>
))}
{!listQ.isLoading &&
rows.map((r: OpinionDto) => (
<tr
key={r.id}
className={selectedId === r.id ? 'is-selected' : ''}
onClick={() => setSelectedId(r.id)}
>
<td>
<span className="kx-list-table__title">{r.title}</span>
{r.secretYn === 'Y' && (
<span className="kx-list-table__muted"> · </span>
)}
</td>
<td className="kx-list-table__muted">{r.category ?? '-'}</td>
<td>
<StatusPill value={r.status} />
</td>
<td className="kx-list-table__muted kx-num">{fmtDateTime(r.createdAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
{listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
{!listQ.isLoading && !listQ.isError && rows.length === 0 && (
<EmptyState title="접수된 의견이 없습니다" />
)}
</section>
</div>
{/* 우: 상세 */}
<aside className="kx-detail" aria-label="의견 상세">
{!selectedId ? (
<EmptyState title="의견을 선택하세요" description="목록에서 항목을 선택하면 상세·답변이 표시됩니다." />
) : detailQ.isLoading ? (
<>
<Skeleton height={24} width="70%" />
<Skeleton height={80} />
</>
) : detailQ.isError ? (
<ErrorState message={errMessage(detailQ.error)} onRetry={() => detailQ.refetch()} />
) : detailQ.data ? (
<>
<div className="kx-detail__head">
<h2 className="kx-detail__title">{detailQ.data.opinion.title}</h2>
<StatusPill value={detailQ.data.opinion.status} />
</div>
<div className="kx-detail__meta">
<span>{detailQ.data.opinion.authorName}</span>
<span>· {detailQ.data.opinion.category}</span>
<span>· {fmtDateTime(detailQ.data.opinion.createdAt)}</span>
</div>
<div className="kx-detail__body">{detailQ.data.opinion.content || '(내용 없음)'}</div>
{isManager && (
<div className="kx-field">
<span className="kx-label"> ()</span>
<div className="kx-noti__chips">
{STATUS_FLOW.map((s) => (
<button
key={s}
className={`kx-chip ${detailQ.data!.opinion.status === s ? 'is-active' : ''}`}
onClick={() => statusM.mutate({ id: detailQ.data!.opinion.id, value: s })}
>
{s}
</button>
))}
</div>
</div>
)}
<div className="kx-thread">
{detailQ.data.comments.length === 0 && (
<p className="kx-list-table__muted"> .</p>
)}
{detailQ.data.comments.map((c) => (
<div className="kx-thread__item" key={c.id}>
<div className="kx-thread__meta">
<span className="kx-thread__author">{c.authorName}</span>
<span>{fmtDateTime(c.createdAt)}</span>
</div>
<div className="kx-thread__text">{c.content}</div>
</div>
))}
{isManager && (
<div className="kx-thread__compose">
<textarea
className="kx-textarea"
placeholder="답변 작성…"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<Button
disabled={!comment.trim() || commentM.isPending}
onClick={() =>
commentM.mutate({ id: detailQ.data!.opinion.id, content: comment })
}
>
</Button>
</div>
)}
</div>
</>
) : null}
</aside>
</div>
{toast}
</div>
);
}

View File

@ -1,251 +0,0 @@
/*
* SCR-46 · (report + stats). 참조: UIWS report/*·stats/*.
* (////) + KPI + + + .
* : /api/work/stats/worklog(fromDate·toDate ), /api/work/reports().
* : 보고서 Jasper PDF PDF disabled. 07_work_api_gaps.md .
*/
import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { reportApi, workStatsApi } from '../../api/endpoints';
import { Button } from '../../components/ui/Button';
import { CHART } from '../chartColors';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconDownload } from '../../components/ui/icons';
import { errMessage, fmtDate } from './workShared';
import './work.css';
type Period = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'YEARLY';
const PERIODS: { v: Period; l: string }[] = [
{ v: 'DAILY', l: '일간' },
{ v: 'WEEKLY', l: '주간' },
{ v: 'MONTHLY', l: '월간' },
{ v: 'QUARTERLY', l: '분기' },
{ v: 'YEARLY', l: '연간' },
];
const BAR_COLORS = [CHART.primary600, CHART.primary700, CHART.aiAccent, CHART.success, CHART.warning, CHART.slate];
function iso(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
/** 기간 → [fromDate, toDate] (역년/ISO 주 기준, 화면 편의 산출). */
function periodRange(p: Period): [string, string] {
const now = new Date();
const y = now.getFullYear();
if (p === 'DAILY') return [iso(now), iso(now)];
if (p === 'WEEKLY') {
const day = now.getDay();
const mon = new Date(now);
mon.setDate(now.getDate() - ((day + 6) % 7));
const sun = new Date(mon);
sun.setDate(mon.getDate() + 6);
return [iso(mon), iso(sun)];
}
if (p === 'MONTHLY') return [iso(new Date(y, now.getMonth(), 1)), iso(new Date(y, now.getMonth() + 1, 0))];
if (p === 'QUARTERLY') {
const q = Math.floor(now.getMonth() / 3);
return [iso(new Date(y, q * 3, 1)), iso(new Date(y, q * 3 + 3, 0))];
}
return [iso(new Date(y, 0, 1)), iso(new Date(y, 11, 31))];
}
export function ReportPage() {
const [period, setPeriod] = useState<Period>('WEEKLY');
const [from, to] = useMemo(() => periodRange(period), [period]);
const statsQ = useQuery({
queryKey: ['work-stats', from, to],
queryFn: () => workStatsApi.worklog({ fromDate: from, toDate: to }),
});
const reportsQ = useQuery({
queryKey: ['reports'],
queryFn: () => reportApi.list({ page: 0, size: 20 }),
});
const stats = statsQ.data;
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title">·</h1>
<p className="kx-work__subtitle">
({from.replace(/-/g, '.')} ~ {to.replace(/-/g, '.')})
</p>
</div>
<div className="kx-work__head-actions">
<div className="kx-seg" role="tablist" aria-label="기간">
{PERIODS.map((p) => (
<button
key={p.v}
role="tab"
aria-selected={period === p.v}
className={`kx-seg__btn ${period === p.v ? 'is-active' : ''}`}
onClick={() => setPeriod(p.v)}
>
{p.l}
</button>
))}
</div>
<span title="백엔드 미지원 — Jasper PDF 연동 후 활성화" style={{ display: 'inline-flex' }}>
<Button variant="secondary" disabled leadingIcon={<IconDownload size={16} />}>
PDF
</Button>
</span>
</div>
</header>
{/* KPI */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
<div className="kx-kpi">
<span className="kx-kpi__label"> </span>
<span className="kx-kpi__value tnum">
{statsQ.isLoading ? <Skeleton height={28} width={80} /> : (stats?.totalCount ?? 0)}
</span>
</div>
<div className="kx-kpi">
<span className="kx-kpi__label"> ()</span>
<span className="kx-kpi__value tnum">
{statsQ.isLoading ? <Skeleton height={28} width={80} /> : (stats?.totalHours ?? 0)}
</span>
</div>
<div className="kx-kpi">
<span className="kx-kpi__label"> </span>
<span className="kx-kpi__value tnum">
{statsQ.isLoading ? <Skeleton height={28} width={80} /> : (stats?.byWriter.length ?? 0)}
</span>
</div>
</div>
{statsQ.isError && (
<ErrorState message={errMessage(statsQ.error)} onRetry={() => statsQ.refetch()} />
)}
{/* 유형별 차트 */}
<section className="kx-card" aria-label="유형별 업무량">
<div className="kx-card__head">
<h2> </h2>
</div>
{statsQ.isLoading ? (
<Skeleton height={260} />
) : !stats || stats.byType.length === 0 ? (
<EmptyState title="집계할 데이터가 없습니다" description="선택 기간에 등록된 업무일지가 없습니다." />
) : (
<div style={{ width: '100%', height: 280 }}>
<ResponsiveContainer>
<BarChart data={stats.byType} margin={{ top: 8, right: 8, bottom: 8, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART.neutral100} vertical={false} />
<XAxis dataKey="name" tick={{ fontSize: 12, fill: CHART.slate }} />
<YAxis tick={{ fontSize: 12, fill: CHART.slate }} allowDecimals={false} />
<Tooltip
contentStyle={{ fontSize: 12, borderRadius: 8, border: `1px solid ${CHART.neutral200}` }}
formatter={(v: number) => [`${v}`, '건수']}
/>
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
{stats.byType.map((_, i) => (
<Cell key={i} fill={BAR_COLORS[i % BAR_COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
</section>
{/* 작성자별 표 + 최근 보고서 */}
<div className="kx-split" style={{ gridTemplateColumns: '1fr 1fr' }}>
<section className="kx-card" aria-label="작성자별 집계">
<div className="kx-card__head">
<h2> </h2>
</div>
<div className="kx-table-scroll">
<table className="kx-list-table">
<thead>
<tr>
<th></th>
<th className="kx-num" style={{ width: 80 }}>
</th>
<th className="kx-num" style={{ width: 90 }}>
(h)
</th>
</tr>
</thead>
<tbody>
{statsQ.isLoading &&
Array.from({ length: 4 }).map((_, i) => (
<tr key={i}>
<td colSpan={3}>
<Skeleton height={20} />
</td>
</tr>
))}
{!statsQ.isLoading &&
(stats?.byWriter ?? []).map((w) => (
<tr key={w.name} style={{ cursor: 'default' }}>
<td className="kx-list-table__title">{w.name}</td>
<td className="kx-num">{w.count}</td>
<td className="kx-num">{w.hours}</td>
</tr>
))}
</tbody>
</table>
</div>
{!statsQ.isLoading && (stats?.byWriter.length ?? 0) === 0 && (
<EmptyState title="집계 데이터 없음" />
)}
</section>
<section className="kx-card" aria-label="최근 보고서">
<div className="kx-card__head">
<h2> </h2>
</div>
<div className="kx-table-scroll">
<table className="kx-list-table">
<thead>
<tr>
<th></th>
<th style={{ width: 80 }}></th>
<th style={{ width: 90 }}></th>
</tr>
</thead>
<tbody>
{reportsQ.isLoading &&
Array.from({ length: 4 }).map((_, i) => (
<tr key={i}>
<td colSpan={3}>
<Skeleton height={20} />
</td>
</tr>
))}
{!reportsQ.isLoading &&
(reportsQ.data?.items ?? []).map((r) => (
<tr key={r.id} style={{ cursor: 'default' }}>
<td className="kx-list-table__title">{r.title}</td>
<td className="kx-list-table__muted">{r.reportType}</td>
<td className="kx-list-table__muted kx-num">{fmtDate(r.createdAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
{reportsQ.isError && (
<ErrorState message={errMessage(reportsQ.error)} onRetry={() => reportsQ.refetch()} />
)}
{!reportsQ.isLoading && !reportsQ.isError && (reportsQ.data?.items.length ?? 0) === 0 && (
<EmptyState title="작성된 보고서가 없습니다" />
)}
</section>
</div>
</div>
);
}

View File

@ -1,179 +0,0 @@
/*
* SCR-44 (search). 참조: UIWS SearchResultsPage.
* + + . : /api/work/search (WORKLOG|NOTICE|MEETING|REPORT|OPINION).
* : AI ·(//) AI UI ( ). 07_work_api_gaps.md .
*/
import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { searchApi } from '../../api/endpoints';
import type { SearchResultItem } from '../../api/types';
import { AiLabel } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import {
IconCalendar,
IconDocument,
IconSearch,
IconSpark,
} from '../../components/ui/icons';
import { fmtDateTime, errMessage } from './workShared';
import './work.css';
const TYPE_META: Record<string, { label: string; Icon: typeof IconDocument }> = {
WORKLOG: { label: '업무일지', Icon: IconDocument },
NOTICE: { label: '공지', Icon: IconDocument },
MEETING: { label: '회의록', Icon: IconCalendar },
REPORT: { label: '업무보고', Icon: IconDocument },
OPINION: { label: '의견', Icon: IconDocument },
};
export function SearchPage() {
const [input, setInput] = useState('');
const [query, setQuery] = useState('');
const [aiMode, setAiMode] = useState(false);
const [tab, setTab] = useState<string>('ALL');
const searchQ = useQuery({
queryKey: ['search', query],
queryFn: () => searchApi.search(query, 50),
enabled: query.trim().length > 0,
});
const results = searchQ.data ?? [];
const counts = useMemo(() => {
const c: Record<string, number> = { ALL: results.length };
for (const r of results) c[r.type] = (c[r.type] ?? 0) + 1;
return c;
}, [results]);
const tabs = useMemo(() => {
const present = Array.from(new Set(results.map((r) => r.type)));
return ['ALL', ...present];
}, [results]);
const filtered = tab === 'ALL' ? results : results.filter((r) => r.type === tab);
function highlight(text: string): React.ReactNode {
if (!query.trim()) return text;
const idx = text.toLowerCase().indexOf(query.toLowerCase());
if (idx < 0) return text;
return (
<>
{text.slice(0, idx)}
<mark style={{ background: '#FEF0C7', padding: '0 1px' }}>
{text.slice(idx, idx + query.length)}
</mark>
{text.slice(idx + query.length)}
</>
);
}
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle">···· </p>
</div>
</header>
<div className="kx-search__hero">
<div className="kx-search__box">
<IconSearch size={22} />
<input
placeholder="검색어를 입력하세요"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && setQuery(input)}
aria-label="통합검색"
autoFocus
/>
<button
className={`kx-search__ai-toggle ${aiMode ? '' : 'is-off'}`}
onClick={() => setAiMode((v) => !v)}
title="AI 자연어 검색(준비 중)"
>
<IconSpark size={14} /> AI
</button>
</div>
<Button onClick={() => setQuery(input)}></Button>
</div>
{aiMode && (
<div className="kx-meet__ai-card" role="note">
<AiLabel>AI </AiLabel>{' '}
<span className="kx-list-table__muted">
. .
</span>
</div>
)}
{!query.trim() ? (
<EmptyState
title="검색어를 입력하세요"
description="업무 데이터 전반을 한 번에 검색합니다."
icon={<IconSearch size={28} />}
/>
) : (
<div className="kx-cal" style={{ gridTemplateColumns: '200px minmax(0,1fr)' }}>
{/* 파셋 */}
<aside className="kx-card" aria-label="검색 필터">
<div className="kx-card__head">
<h2></h2>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{tabs.map((t) => (
<button
key={t}
className={`kx-msg__rail-btn ${tab === t ? 'is-active' : ''}`}
onClick={() => setTab(t)}
>
{t === 'ALL' ? '전체' : (TYPE_META[t]?.label ?? t)}
<span className="kx-list-table__muted">{counts[t] ?? 0}</span>
</button>
))}
</div>
</aside>
{/* 결과 */}
<section aria-label="검색 결과" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{searchQ.isLoading &&
Array.from({ length: 5 }).map((_, i) => (
<div className="kx-search__result" key={i}>
<Skeleton height={36} width={36} radius={6} />
<div style={{ flex: 1 }}>
<Skeleton height={16} width="50%" />
<Skeleton height={14} width="80%" />
</div>
</div>
))}
{searchQ.isError && (
<ErrorState message={errMessage(searchQ.error)} onRetry={() => searchQ.refetch()} />
)}
{!searchQ.isLoading && !searchQ.isError && filtered.length === 0 && (
<EmptyState title="검색 결과가 없습니다" description={`'${query}'에 대한 결과를 찾지 못했습니다.`} />
)}
{filtered.map((r: SearchResultItem) => {
const meta = TYPE_META[r.type] ?? { label: r.type, Icon: IconDocument };
const Icon = meta.Icon;
return (
<div className="kx-search__result" key={`${r.type}-${r.id}`}>
<span className="kx-search__result-icon">
<Icon size={18} />
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div className="kx-list-table__title">{highlight(r.title)}</div>
<div className="kx-detail__meta">
<span className="kx-pill kx-pill--info">{meta.label}</span>
<span>{fmtDateTime(r.createdAt)}</span>
</div>
</div>
</div>
);
})}
</section>
</div>
)}
</div>
);
}

View File

@ -1,322 +0,0 @@
/*
* SCR-40 · (schedule). 참조: UIWS schedule/PersonalSchedulePage.
* + /. · ( SCR-15 ).
* : /api/work/schedules (fromAt/toAt ISO ). .
*/
import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { scheduleWorkApi } from '../../api/endpoints';
import type { ScheduleDto, ScheduleSaveRequest } from '../../api/types';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconChevronLeft, IconChevronRight, IconPlus } from '../../components/ui/icons';
import { errMessage, fmtDateTime, useToast } from './workShared';
import './work.css';
const WD = ['일', '월', '화', '수', '목', '금', '토'];
const TYPE_COLOR: Record<string, string> = {
milestone: '#0066B3',
auction: '#6D4AFF',
movein: '#0E8A5F',
meeting: '#B45309',
personal: '#667085',
};
const TYPE_LABEL: Record<string, string> = {
milestone: '행사 마일스톤',
auction: '옥션 마감',
movein: '반입 슬롯',
meeting: '회의',
personal: '개인',
};
function ymd(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function colorOf(s: ScheduleDto): string {
return s.color || TYPE_COLOR[(s.scheduleType ?? '').toLowerCase()] || '#0066B3';
}
export function WorkSchedulePage() {
const qc = useQueryClient();
const { show, node: toast } = useToast();
const today = new Date();
const [cursor, setCursor] = useState({ y: today.getFullYear(), m: today.getMonth() });
const [selDate, setSelDate] = useState(ymd(today));
const [composing, setComposing] = useState(false);
const [form, setForm] = useState<ScheduleSaveRequest>({
title: '',
scheduleType: 'personal',
startAt: `${ymd(today)}T09:00`,
endAt: `${ymd(today)}T10:00`,
allDay: false,
location: '',
content: '',
});
// 표시 그리드(월 시작 주 일요일 ~ 6주) 범위로 조회.
const grid = useMemo(() => {
const first = new Date(cursor.y, cursor.m, 1);
const start = new Date(first);
start.setDate(1 - first.getDay());
const days: Date[] = [];
for (let i = 0; i < 42; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
days.push(d);
}
return { start: days[0], end: days[41], days };
}, [cursor]);
const listQ = useQuery({
queryKey: ['schedules', ymd(grid.start), ymd(grid.end)],
queryFn: () =>
scheduleWorkApi.list({
fromAt: `${ymd(grid.start)}T00:00:00`,
toAt: `${ymd(grid.end)}T23:59:59`,
}),
});
const byDay = useMemo(() => {
const map = new Map<string, ScheduleDto[]>();
for (const s of listQ.data ?? []) {
const key = (s.startAt ?? '').slice(0, 10);
if (!key) continue;
const arr = map.get(key) ?? [];
arr.push(s);
map.set(key, arr);
}
return map;
}, [listQ.data]);
const createM = useMutation({
mutationFn: (body: ScheduleSaveRequest) => scheduleWorkApi.create(body),
onSuccess: () => {
show('일정이 추가되었습니다.');
setComposing(false);
qc.invalidateQueries({ queryKey: ['schedules'] });
},
onError: (e) => show(errMessage(e)),
});
const delM = useMutation({
mutationFn: (id: string) => scheduleWorkApi.remove(id),
onSuccess: () => {
show('삭제되었습니다.');
qc.invalidateQueries({ queryKey: ['schedules'] });
},
onError: (e) => show(errMessage(e)),
});
const agenda = byDay.get(selDate) ?? [];
const todayKey = ymd(today);
function openCompose() {
setForm({
title: '',
scheduleType: 'personal',
startAt: `${selDate}T09:00`,
endAt: `${selDate}T10:00`,
allDay: false,
location: '',
content: '',
});
setComposing(true);
}
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle">· · // </p>
</div>
<div className="kx-work__head-actions">
<Button
variant="ghost"
onClick={() =>
setCursor((c) => (c.m === 0 ? { y: c.y - 1, m: 11 } : { y: c.y, m: c.m - 1 }))
}
aria-label="이전 달"
leadingIcon={<IconChevronLeft size={16} />}
>
</Button>
<strong style={{ minWidth: 120, textAlign: 'center' }}>
{cursor.y} {cursor.m + 1}
</strong>
<Button
variant="ghost"
onClick={() =>
setCursor((c) => (c.m === 11 ? { y: c.y + 1, m: 0 } : { y: c.y, m: c.m + 1 }))
}
aria-label="다음 달"
>
<IconChevronRight size={16} />
</Button>
</div>
</header>
<div className="kx-cal">
<div>
<div className="kx-cal__grid" role="grid" aria-label="월 캘린더">
<div className="kx-cal__weekhead" role="row">
{WD.map((w, i) => (
<div key={w} className={`kx-cal__wd ${i === 0 ? 'kx-cal__wd--sun' : ''}`} role="columnheader">
{w}
</div>
))}
</div>
{Array.from({ length: 6 }).map((_, wk) => (
<div className="kx-cal__week" role="row" key={wk}>
{grid.days.slice(wk * 7, wk * 7 + 7).map((d) => {
const key = ymd(d);
const evts = byDay.get(key) ?? [];
const isOther = d.getMonth() !== cursor.m;
return (
<div
key={key}
role="gridcell"
className={`kx-cal__cell ${isOther ? 'is-other' : ''} ${
key === selDate ? 'is-selected' : ''
} ${key === todayKey ? 'is-today' : ''}`}
onClick={() => setSelDate(key)}
>
<span className="kx-cal__daynum">{d.getDate()}</span>
{listQ.isLoading && isOther === false && wk === 1 && (
<Skeleton height={14} />
)}
{evts.slice(0, 3).map((s) => (
<span
key={s.id}
className="kx-cal__event"
style={{ background: colorOf(s) }}
title={s.title}
>
{s.title}
</span>
))}
{evts.length > 3 && <span className="kx-cal__more">+{evts.length - 3}</span>}
</div>
);
})}
</div>
))}
</div>
<div className="kx-cal__legend" style={{ marginTop: 12 }}>
{Object.entries(TYPE_LABEL).map(([k, label]) => (
<span key={k}>
<span className="kx-cal__swatch" style={{ background: TYPE_COLOR[k] }} />
{label}
</span>
))}
</div>
{listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
</div>
{/* 어젠다 */}
<aside className="kx-cal__agenda" aria-label={`${selDate} 일정`}>
<div className="kx-detail__head">
<h2 className="kx-detail__title">{selDate.replace(/-/g, '.')}</h2>
<Button leadingIcon={<IconPlus size={16} />} onClick={openCompose}>
</Button>
</div>
{composing ? (
<div className="kx-formgrid" style={{ gridTemplateColumns: '1fr' }}>
<label className="kx-field">
<span className="kx-label"> *</span>
<input
className="kx-input"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<select
className="kx-select"
value={form.scheduleType ?? 'personal'}
onChange={(e) => setForm({ ...form, scheduleType: e.target.value })}
>
{Object.entries(TYPE_LABEL).map(([k, l]) => (
<option key={k} value={k}>
{l}
</option>
))}
</select>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<input
type="datetime-local"
className="kx-input"
value={form.startAt}
onChange={(e) => setForm({ ...form, startAt: e.target.value })}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<input
type="datetime-local"
className="kx-input"
value={form.endAt ?? ''}
onChange={(e) => setForm({ ...form, endAt: e.target.value })}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<input
className="kx-input"
value={form.location ?? ''}
onChange={(e) => setForm({ ...form, location: e.target.value })}
/>
</label>
<div className="kx-detail__actions">
<Button
disabled={!form.title.trim() || createM.isPending}
onClick={() => createM.mutate(form)}
>
{createM.isPending ? '저장 중…' : '저장'}
</Button>
<Button variant="ghost" onClick={() => setComposing(false)}>
</Button>
</div>
</div>
) : agenda.length === 0 ? (
<EmptyState title="일정이 없습니다" description="이 날짜에 등록된 일정이 없습니다." />
) : (
agenda.map((s) => (
<div
key={s.id}
className="kx-cal__agenda-item"
style={{ borderLeftColor: colorOf(s) }}
>
<div style={{ flex: 1 }}>
<div className="kx-list-table__title">{s.title}</div>
<div className="kx-detail__meta">
<span>{s.allDay ? '종일' : fmtDateTime(s.startAt)}</span>
{s.location && <span>· {s.location}</span>}
<span>· {TYPE_LABEL[(s.scheduleType ?? '').toLowerCase()] ?? s.scheduleType ?? ''}</span>
</div>
</div>
<button
className="kx-tag"
onClick={() => {
if (window.confirm('이 일정을 삭제할까요?')) delM.mutate(s.id);
}}
>
</button>
</div>
))
)}
</aside>
</div>
{toast}
</div>
);
}

View File

@ -1,339 +0,0 @@
/*
* SCR-39 (worklog). 참조: UIWS worklog/*.
* : 필터 + (····) / : 상세· (·).
* : /api/work/worklogs (PageResponse). 3(//) .
* : worklog 07_work_api_gaps.md .
*/
import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { worklogApi } from '../../api/endpoints';
import type { WorklogDto, WorklogSaveRequest } from '../../api/types';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconPlus } from '../../components/ui/icons';
import { StatusPill, errMessage, fmtDate, useToast } from './workShared';
import './work.css';
const STATUS_OPTS = [
{ v: '', l: '전체' },
{ v: '진행중', l: '진행중' },
{ v: '완료', l: '완료' },
{ v: '보류', l: '보류' },
];
const TYPE_OPTS = ['현장점검', '시공관리', '설계검토', '행정', '회의', '기타'];
const EMPTY_FORM: WorklogSaveRequest = {
workDate: new Date().toISOString().slice(0, 10),
title: '',
workType: '현장점검',
progress: '',
status: '진행중',
content: '',
hours: 0,
};
export function WorklogPage() {
const qc = useQueryClient();
const { show, node: toast } = useToast();
const [fromDate, setFromDate] = useState('');
const [toDate, setToDate] = useState('');
const [status, setStatus] = useState('');
const [page, setPage] = useState(0);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [form, setForm] = useState<WorklogSaveRequest>(EMPTY_FORM);
const listQ = useQuery({
queryKey: ['worklogs', { fromDate, toDate, status, page }],
queryFn: () =>
worklogApi.list({
fromDate: fromDate || undefined,
toDate: toDate || undefined,
status: status || undefined,
page,
size: 20,
}),
});
const invalidate = () => qc.invalidateQueries({ queryKey: ['worklogs'] });
const saveM = useMutation({
mutationFn: (body: WorklogSaveRequest) =>
selectedId ? worklogApi.update(selectedId, body) : worklogApi.create(body),
onSuccess: (saved: WorklogDto) => {
show(selectedId ? '수정되었습니다.' : '등록되었습니다.');
setSelectedId(saved.id);
invalidate();
},
onError: (e) => show(errMessage(e)),
});
const delM = useMutation({
mutationFn: (id: string) => worklogApi.remove(id),
onSuccess: () => {
show('삭제되었습니다.');
resetForm();
invalidate();
},
onError: (e) => show(errMessage(e)),
});
function resetForm() {
setSelectedId(null);
setForm(EMPTY_FORM);
}
function selectRow(row: WorklogDto) {
setSelectedId(row.id);
setForm({
eventId: row.eventId ?? undefined,
workDate: row.workDate,
title: row.title,
workType: row.workType ?? '',
progress: row.progress ?? '',
status: row.status ?? '',
content: row.content ?? '',
hours: row.hours,
});
}
const rows = listQ.data?.items ?? [];
const total = listQ.data?.total ?? 0;
const totalPages = Math.max(1, Math.ceil(total / 20));
const canSave = form.title.trim().length > 0 && !!form.workDate;
const editingTitle = useMemo(() => (selectedId ? '업무일지 수정' : '새 업무일지'), [selectedId]);
return (
<div className="kx-page">
<header className="kx-work__head">
<div>
<h1 className="kx-work__title"></h1>
<p className="kx-work__subtitle">·· · / </p>
</div>
<Button leadingIcon={<IconPlus size={16} />} onClick={resetForm}>
</Button>
</header>
<div className="kx-split">
{/* 목록 */}
<section className="kx-card" aria-label="업무일지 목록">
<div className="kx-filterbar">
<label className="kx-field">
<span className="kx-label"></span>
<input
type="date"
className="kx-input"
value={fromDate}
onChange={(e) => {
setFromDate(e.target.value);
setPage(0);
}}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<input
type="date"
className="kx-input"
value={toDate}
onChange={(e) => {
setToDate(e.target.value);
setPage(0);
}}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<select
className="kx-select"
value={status}
onChange={(e) => {
setStatus(e.target.value);
setPage(0);
}}
>
{STATUS_OPTS.map((o) => (
<option key={o.v} value={o.v}>
{o.l}
</option>
))}
</select>
</label>
</div>
<div className="kx-table-scroll kx-scroll-70">
<table className="kx-list-table">
<thead>
<tr>
<th style={{ width: 72 }}></th>
<th></th>
<th style={{ width: 92 }}></th>
<th style={{ width: 92 }}></th>
<th style={{ width: 64 }} className="kx-num">
</th>
</tr>
</thead>
<tbody>
{listQ.isLoading &&
Array.from({ length: 6 }).map((_, i) => (
<tr key={i}>
<td colSpan={5}>
<Skeleton height={20} />
</td>
</tr>
))}
{!listQ.isLoading &&
rows.map((r) => (
<tr
key={r.id}
className={selectedId === r.id ? 'is-selected' : ''}
onClick={() => selectRow(r)}
>
<td className="kx-num">{fmtDate(r.workDate)}</td>
<td>
<div className="kx-list-table__title">{r.title}</div>
<div className="kx-list-table__muted">{r.writerName}</div>
</td>
<td className="kx-list-table__muted">{r.workType ?? '-'}</td>
<td>
<StatusPill value={r.status} />
</td>
<td className="kx-num">{r.hours ?? 0}h</td>
</tr>
))}
</tbody>
</table>
</div>
{!listQ.isLoading && listQ.isError && (
<ErrorState message={errMessage(listQ.error)} onRetry={() => listQ.refetch()} />
)}
{!listQ.isLoading && !listQ.isError && rows.length === 0 && (
<EmptyState
title="작성된 업무일지가 없습니다"
description="우측에서 새 일지를 작성하세요."
/>
)}
{totalPages > 1 && (
<div className="kx-pager">
<button disabled={page === 0} onClick={() => setPage((p) => Math.max(0, p - 1))}>
</button>
<span className="tnum">
{page + 1} / {totalPages}
</span>
<button
disabled={page + 1 >= totalPages}
onClick={() => setPage((p) => p + 1)}
>
</button>
</div>
)}
</section>
{/* 상세·작성 */}
<aside className="kx-detail" aria-label="업무일지 작성">
<div className="kx-detail__head">
<h2 className="kx-detail__title">{editingTitle}</h2>
{selectedId && (
<Button
variant="danger"
onClick={() => {
if (window.confirm('이 일지를 삭제할까요?')) delM.mutate(selectedId);
}}
>
</Button>
)}
</div>
<div className="kx-formgrid">
<label className="kx-field">
<span className="kx-label"> *</span>
<input
type="date"
className="kx-input"
value={form.workDate}
onChange={(e) => setForm({ ...form, workDate: e.target.value })}
/>
</label>
<label className="kx-field">
<span className="kx-label">()</span>
<input
type="number"
min={0}
step={0.5}
className="kx-input"
value={form.hours ?? 0}
onChange={(e) => setForm({ ...form, hours: Number(e.target.value) })}
/>
</label>
<label className="kx-field kx-field--full">
<span className="kx-label"> *</span>
<input
className="kx-input"
value={form.title}
maxLength={200}
placeholder="업무 제목"
onChange={(e) => setForm({ ...form, title: e.target.value })}
/>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<select
className="kx-select"
value={form.workType ?? ''}
onChange={(e) => setForm({ ...form, workType: e.target.value })}
>
{TYPE_OPTS.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</label>
<label className="kx-field">
<span className="kx-label"></span>
<select
className="kx-select"
value={form.status ?? ''}
onChange={(e) => setForm({ ...form, status: e.target.value })}
>
{STATUS_OPTS.filter((o) => o.v).map((o) => (
<option key={o.v} value={o.v}>
{o.l}
</option>
))}
</select>
</label>
<label className="kx-field kx-field--full">
<span className="kx-label">·</span>
<textarea
className="kx-textarea"
value={form.content ?? ''}
placeholder="진행 내용, 이슈, 특이사항을 기록하세요."
onChange={(e) => setForm({ ...form, content: e.target.value })}
/>
</label>
</div>
<div className="kx-detail__actions">
<Button disabled={!canSave || saveM.isPending} onClick={() => saveM.mutate(form)}>
{saveM.isPending ? '저장 중…' : selectedId ? '수정 저장' : '등록'}
</Button>
{selectedId && (
<Button variant="ghost" onClick={resetForm}>
</Button>
)}
</div>
</aside>
</div>
{toast}
</div>
);
}

View File

@ -1,855 +0,0 @@
/*
* §5B 공통 업무 기능(SCR-39~48) 공유 스타일.
* design.md §1 토큰만 참조. shared.css 프리미티브(kx-page·kx-card·kx-seg·kx-select·kx-kpi) 위에 얹는다.
*/
@import '../shared.css';
/* ── 페이지 헤더 ── */
.kx-work__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.kx-work__title {
font-size: var(--fs-h1);
line-height: var(--lh-h1);
color: var(--color-neutral-900);
}
.kx-work__subtitle {
margin-top: 2px;
font-size: var(--fs-body);
color: var(--color-neutral-500);
}
.kx-work__head-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
/* ── 목록/상세 2-Pane 분할 (worklog·opinion) ── */
.kx-split {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(300px, 1fr);
gap: var(--space-4);
align-items: start;
}
@media (max-width: 1100px) {
.kx-split {
grid-template-columns: 1fr;
}
}
/* ── 필터 바 ── */
.kx-filterbar {
display: flex;
align-items: flex-end;
gap: var(--space-3);
flex-wrap: wrap;
margin-bottom: var(--space-3);
}
.kx-field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.kx-field--grow {
flex: 1;
}
.kx-label {
font-size: var(--fs-caption);
font-weight: 600;
color: var(--color-neutral-500);
}
.kx-input,
.kx-textarea {
height: 36px;
padding: 0 var(--space-3);
border: var(--border-card);
border-radius: var(--radius-sm);
background: var(--color-white);
font-size: var(--fs-body);
color: var(--color-neutral-900);
font-family: inherit;
width: 100%;
}
.kx-textarea {
height: auto;
min-height: 96px;
padding: var(--space-2) var(--space-3);
line-height: var(--lh-body);
resize: vertical;
}
.kx-input:focus,
.kx-textarea:focus,
.kx-select:focus {
outline: 2px solid var(--color-primary-100);
border-color: var(--color-primary-600);
}
/* ── 데이터 테이블 ── */
.kx-list-table {
width: 100%;
border-collapse: collapse;
font-size: var(--fs-body);
}
.kx-list-table thead th {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-neutral-050);
text-align: left;
padding: 0 var(--space-3);
height: 40px;
font-size: var(--fs-caption);
font-weight: 600;
color: var(--color-neutral-500);
border-bottom: var(--border-card);
white-space: nowrap;
}
.kx-list-table tbody td {
padding: 0 var(--space-3);
height: 44px;
border-bottom: 1px solid var(--color-neutral-100);
color: var(--color-neutral-700);
vertical-align: middle;
}
.kx-list-table tbody tr:nth-child(even) {
background: var(--zebra);
}
.kx-list-table tbody tr {
cursor: pointer;
}
.kx-list-table tbody tr:hover {
background: var(--color-primary-050);
}
.kx-list-table tbody tr.is-selected {
background: var(--color-primary-100);
}
.kx-list-table__title {
font-weight: 600;
color: var(--color-neutral-900);
}
.kx-list-table__muted {
color: var(--color-neutral-500);
}
.kx-num {
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ── 상태 필/배지(경량) ── */
.kx-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 10px;
border-radius: var(--radius-pill);
font-size: var(--fs-caption);
font-weight: 600;
white-space: nowrap;
}
.kx-pill--neutral {
background: var(--color-neutral-100);
color: var(--color-neutral-700);
}
.kx-pill--info {
background: var(--color-primary-050);
color: var(--color-primary-700);
}
.kx-pill--warn {
background: var(--color-violation-warn-bg);
color: var(--color-violation-warn-text);
}
.kx-pill--success {
background: #e6f4ee;
color: var(--color-success);
}
.kx-pill--danger {
background: #fef3f2;
color: var(--color-error);
}
/* ── 상세/작성 패널 ── */
.kx-detail {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
position: sticky;
top: 0;
}
.kx-detail__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
.kx-detail__title {
font-size: var(--fs-h3);
color: var(--color-neutral-900);
}
.kx-detail__meta {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.kx-detail__body {
font-size: var(--fs-body);
line-height: var(--lh-body);
color: var(--color-neutral-700);
white-space: pre-wrap;
word-break: break-word;
}
.kx-detail__actions {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
/* ── 폼 그리드 ── */
.kx-formgrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
.kx-formgrid .kx-field--full {
grid-column: 1 / -1;
}
/* ── 댓글/스레드 ── */
.kx-thread {
display: flex;
flex-direction: column;
gap: var(--space-3);
border-top: var(--border-card);
padding-top: var(--space-3);
}
.kx-thread__item {
display: flex;
flex-direction: column;
gap: 2px;
}
.kx-thread__meta {
display: flex;
gap: var(--space-2);
font-size: var(--fs-caption);
color: var(--color-neutral-500);
}
.kx-thread__author {
font-weight: 600;
color: var(--color-neutral-700);
}
.kx-thread__text {
font-size: var(--fs-body);
color: var(--color-neutral-700);
line-height: var(--lh-body);
}
.kx-thread__compose {
display: flex;
gap: var(--space-2);
align-items: flex-end;
}
.kx-thread__compose .kx-textarea {
min-height: 60px;
}
/* ── SCR-41 쪽지 3-Pane ── */
.kx-msg {
display: grid;
grid-template-columns: 200px minmax(0, 1.4fr) minmax(320px, 1.2fr);
gap: var(--space-4);
align-items: start;
}
@media (max-width: 1200px) {
.kx-msg {
grid-template-columns: 180px 1fr;
}
.kx-msg__read {
grid-column: 1 / -1;
}
}
.kx-msg__rail {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
padding: var(--space-2);
display: flex;
flex-direction: column;
gap: 2px;
}
.kx-msg__rail-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: 8px var(--space-3);
border: none;
background: transparent;
border-radius: var(--radius-sm);
font-size: var(--fs-body);
font-weight: 600;
color: var(--color-neutral-700);
cursor: pointer;
text-align: left;
}
.kx-msg__rail-btn.is-active {
background: var(--color-primary-050);
color: var(--color-primary-700);
}
.kx-msg__rail-count {
font-size: var(--fs-caption);
background: var(--color-primary-600);
color: #fff;
border-radius: var(--radius-pill);
padding: 0 7px;
min-width: 18px;
text-align: center;
}
.kx-msg__list {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
overflow: hidden;
max-height: 70vh;
overflow-y: auto;
}
.kx-msg__item {
display: flex;
flex-direction: column;
gap: 2px;
padding: var(--space-3);
border-bottom: 1px solid var(--color-neutral-100);
cursor: pointer;
}
.kx-msg__item:hover {
background: var(--color-primary-050);
}
.kx-msg__item.is-selected {
background: var(--color-primary-100);
}
.kx-msg__item.is-unread .kx-msg__item-title {
font-weight: 700;
color: var(--color-neutral-900);
}
.kx-msg__item-top {
display: flex;
justify-content: space-between;
gap: var(--space-2);
}
.kx-msg__item-sender {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
}
.kx-msg__item-title {
font-size: var(--fs-body);
color: var(--color-neutral-700);
}
.kx-msg__item-time {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
white-space: nowrap;
}
.kx-msg__read {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.kx-msg__unread-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-primary-600);
display: inline-block;
flex-shrink: 0;
}
/* ── 태그 입력(수신자 등) ── */
.kx-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 6px;
border: var(--border-card);
border-radius: var(--radius-sm);
min-height: 36px;
}
.kx-tag {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--color-primary-050);
color: var(--color-primary-700);
border-radius: var(--radius-pill);
padding: 2px 6px 2px 10px;
font-size: var(--fs-caption);
font-weight: 600;
}
.kx-tag button {
border: none;
background: transparent;
color: var(--color-primary-700);
cursor: pointer;
display: inline-flex;
padding: 0;
}
.kx-tags input {
border: none;
outline: none;
flex: 1;
min-width: 120px;
font-size: var(--fs-body);
background: transparent;
}
/* ── SCR-40 캘린더 ── */
.kx-cal {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: var(--space-4);
align-items: start;
}
@media (max-width: 1100px) {
.kx-cal {
grid-template-columns: 1fr;
}
}
.kx-cal__grid {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
overflow: hidden;
}
.kx-cal__weekhead,
.kx-cal__week {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.kx-cal__weekhead {
border-bottom: var(--border-card);
}
.kx-cal__wd {
padding: 8px;
text-align: center;
font-size: var(--fs-caption);
font-weight: 600;
color: var(--color-neutral-500);
}
.kx-cal__wd--sun {
color: var(--color-error);
}
.kx-cal__cell {
min-height: 96px;
border-right: 1px solid var(--color-neutral-100);
border-bottom: 1px solid var(--color-neutral-100);
padding: 4px;
display: flex;
flex-direction: column;
gap: 3px;
cursor: pointer;
background: var(--color-white);
}
.kx-cal__cell:hover {
background: var(--color-primary-050);
}
.kx-cal__cell.is-other {
background: var(--color-neutral-050);
}
.kx-cal__cell.is-selected {
outline: 2px solid var(--color-primary-600);
outline-offset: -2px;
}
.kx-cal__daynum {
font-size: var(--fs-caption);
font-weight: 600;
color: var(--color-neutral-700);
}
.kx-cal__cell.is-today .kx-cal__daynum {
background: var(--color-primary-600);
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.kx-cal__event {
font-size: 11px;
line-height: 16px;
padding: 1px 6px;
border-radius: 4px;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.kx-cal__more {
font-size: 11px;
color: var(--color-neutral-500);
}
.kx-cal__legend {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
font-size: var(--fs-caption);
color: var(--color-neutral-500);
}
.kx-cal__legend span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.kx-cal__swatch {
width: 10px;
height: 10px;
border-radius: 3px;
display: inline-block;
}
.kx-cal__agenda {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.kx-cal__agenda-item {
display: flex;
gap: var(--space-2);
padding: var(--space-2);
border-radius: var(--radius-sm);
border-left: 3px solid var(--color-primary-600);
background: var(--color-neutral-050);
}
/* ── SCR-44 검색 ── */
.kx-search__hero {
display: flex;
gap: var(--space-2);
align-items: center;
}
.kx-search__box {
flex: 1;
display: flex;
align-items: center;
gap: var(--space-2);
height: 52px;
padding: 0 var(--space-4);
border: 1.5px solid var(--color-neutral-200);
border-radius: var(--radius-lg);
background: var(--color-white);
}
.kx-search__box:focus-within {
border-color: var(--color-primary-600);
box-shadow: 0 0 0 3px var(--color-primary-050);
}
.kx-search__box input {
flex: 1;
border: none;
outline: none;
font-size: var(--fs-h3);
background: transparent;
}
.kx-search__ai-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: var(--radius-pill);
border: 1px solid var(--color-ai-accent);
background: var(--color-ai-surface);
color: var(--color-ai-accent);
font-size: var(--fs-caption);
font-weight: 700;
cursor: pointer;
white-space: nowrap;
}
.kx-search__ai-toggle.is-off {
border-color: var(--color-neutral-200);
background: var(--color-white);
color: var(--color-neutral-500);
}
.kx-search__tabs {
display: flex;
gap: var(--space-1);
border-bottom: var(--border-card);
flex-wrap: wrap;
}
.kx-search__tab {
padding: 8px 14px;
border: none;
background: transparent;
font-size: var(--fs-body);
font-weight: 600;
color: var(--color-neutral-500);
cursor: pointer;
border-bottom: 2px solid transparent;
}
.kx-search__tab.is-active {
color: var(--color-primary-700);
border-bottom-color: var(--color-primary-600);
}
.kx-search__result {
display: flex;
gap: var(--space-3);
padding: var(--space-3);
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
cursor: pointer;
}
.kx-search__result:hover {
border-color: var(--color-primary-600);
background: var(--color-primary-050);
}
.kx-search__result-icon {
width: 36px;
height: 36px;
border-radius: var(--radius-sm);
background: var(--color-primary-050);
color: var(--color-primary-600);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* ── SCR-45 회의록 ── */
.kx-meet {
display: grid;
grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
gap: var(--space-4);
align-items: start;
}
@media (max-width: 1100px) {
.kx-meet {
grid-template-columns: 1fr;
}
}
.kx-meet__list-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: var(--space-3);
border-bottom: 1px solid var(--color-neutral-100);
cursor: pointer;
}
.kx-meet__list-item:hover {
background: var(--color-primary-050);
}
.kx-meet__list-item.is-selected {
background: var(--color-primary-100);
}
.kx-meet__ai-card {
border: 1px solid var(--color-ai-accent);
background: var(--color-ai-surface);
border-radius: var(--radius-lg);
padding: var(--space-3);
}
.kx-meet__transcript {
max-height: 320px;
overflow-y: auto;
border: var(--border-card);
border-radius: var(--radius-sm);
padding: var(--space-3);
background: var(--color-neutral-050);
font-size: var(--fs-body);
line-height: var(--lh-body);
white-space: pre-wrap;
}
.kx-meet__player {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
border: var(--border-card);
border-radius: var(--radius-sm);
background: var(--color-neutral-050);
color: var(--color-neutral-500);
font-size: var(--fs-body);
}
/* ── SCR-47 알림센터 ── */
.kx-noti__chips {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.kx-chip {
padding: 6px 14px;
border-radius: var(--radius-pill);
border: var(--border-card);
background: var(--color-white);
font-size: var(--fs-caption);
font-weight: 600;
color: var(--color-neutral-700);
cursor: pointer;
}
.kx-chip.is-active {
background: var(--color-primary-600);
border-color: var(--color-primary-600);
color: #fff;
}
.kx-noti__item {
display: flex;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-neutral-100);
align-items: flex-start;
}
.kx-noti__item.is-unread {
background: var(--color-primary-050);
}
.kx-noti__icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--color-primary-050);
color: var(--color-primary-600);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.kx-noti__body {
flex: 1;
min-width: 0;
}
.kx-noti__title {
font-weight: 600;
color: var(--color-neutral-900);
font-size: var(--fs-body);
}
.kx-noti__desc {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
margin-top: 2px;
}
.kx-noti__time {
font-size: var(--fs-caption);
color: var(--color-neutral-500);
white-space: nowrap;
}
.kx-noti__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-primary-600);
margin-top: 6px;
flex-shrink: 0;
}
/* ── SCR-48 마이페이지 ── */
.kx-my {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: var(--space-4);
align-items: start;
}
@media (max-width: 900px) {
.kx-my {
grid-template-columns: 1fr;
}
}
.kx-my__rail {
border: var(--border-card);
border-radius: var(--radius-lg);
background: var(--color-white);
padding: var(--space-2);
display: flex;
flex-direction: column;
gap: 2px;
}
.kx-my__rail-btn {
text-align: left;
padding: 10px var(--space-3);
border: none;
background: transparent;
border-radius: var(--radius-sm);
font-size: var(--fs-body);
font-weight: 600;
color: var(--color-neutral-700);
cursor: pointer;
}
.kx-my__rail-btn.is-active {
background: var(--color-primary-050);
color: var(--color-primary-700);
}
.kx-my__avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--color-primary-600);
color: #fff;
font-size: 26px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.kx-switch {
display: inline-flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
/* ── 토스트 ── */
.kx-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 60;
background: var(--color-neutral-900);
color: #fff;
padding: 10px 18px;
border-radius: var(--radius-lg);
font-size: var(--fs-body);
box-shadow: 0 6px 20px rgba(16, 24, 40, 0.24);
}
/* ── 페이지네이션 ── */
.kx-pager {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) 0 0;
font-size: var(--fs-body);
color: var(--color-neutral-700);
}
.kx-pager button {
height: 32px;
min-width: 32px;
padding: 0 10px;
border: var(--border-card);
border-radius: var(--radius-sm);
background: var(--color-white);
cursor: pointer;
color: var(--color-neutral-700);
}
.kx-pager button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
/* 스크롤 컨테이너 상한 */
.kx-scroll-70 {
max-height: 70vh;
overflow-y: auto;
}

View File

@ -1,85 +0,0 @@
/*
* §5B · ·.
* (), .
*/
import { useCallback, useState } from 'react';
import { ApiRequestError } from '../../api/client';
/** ISO/DB 문자열 → "MM.DD" (실패 시 원문 앞 10자). */
export function fmtDate(s: string | null | undefined): string {
if (!s) return '-';
const d = new Date(s);
if (Number.isNaN(d.getTime())) return s.slice(0, 10);
return `${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
}
/** ISO/DB 문자열 → "YYYY.MM.DD HH:mm". */
export function fmtDateTime(s: string | null | undefined): string {
if (!s) return '-';
const d = new Date(s);
if (Number.isNaN(d.getTime())) return s;
const p = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}.${p(d.getMonth() + 1)}.${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
/** 상대 시간(방금·N분·N시간·N일 전). */
export function timeAgo(s: string | null | undefined): string {
if (!s) return '';
const d = new Date(s);
if (Number.isNaN(d.getTime())) return fmtDate(s);
const diff = Date.now() - d.getTime();
const min = Math.floor(diff / 60000);
if (min < 1) return '방금';
if (min < 60) return `${min}분 전`;
const hr = Math.floor(min / 60);
if (hr < 24) return `${hr}시간 전`;
const day = Math.floor(hr / 24);
if (day < 7) return `${day}일 전`;
return fmtDate(s);
}
/** 봉투 오류 → 한국어 메시지(코드 우선). */
export function errMessage(e: unknown): string {
if (e instanceof ApiRequestError) {
if (e.code === 'FORBIDDEN') return '권한이 없습니다.';
if (e.code === 'NOT_FOUND') return '대상을 찾을 수 없습니다.';
if (e.code === 'NOT_IMPLEMENTED') return '준비 중인 기능입니다.';
return e.message;
}
if (e instanceof Error) return e.message;
return '요청을 처리하지 못했습니다.';
}
type PillTone = 'neutral' | 'info' | 'warn' | 'success' | 'danger';
/** 상태 코드 → 필 톤·라벨(백엔드 원문 라벨 표기, 알려진 코드만 톤 부여). */
export function statusPillTone(raw: string | null | undefined): PillTone {
const v = (raw ?? '').toUpperCase();
if (['DONE', 'ANSWERED', 'COMPLETED', 'COMPLETE', '완료', '답변완료', 'CLOSED'].includes(v)) return 'success';
if (['REVIEWING', 'IN_PROGRESS', 'PROGRESS', '진행중', '검토중', 'OPEN'].includes(v)) return 'info';
if (['RECEIVED', '접수', 'PENDING', 'WAIT'].includes(v)) return 'neutral';
if (['REJECTED', '반려', 'BLOCKED', 'FAILED'].includes(v)) return 'danger';
if (['HOLD', 'DELAYED', '보류'].includes(v)) return 'warn';
return 'neutral';
}
/** 상태 필 — 라벨은 원문 유지. */
export function StatusPill({ value }: { value: string | null | undefined }) {
if (!value) return <span className="kx-pill kx-pill--neutral">-</span>;
return <span className={`kx-pill kx-pill--${statusPillTone(value)}`}>{value}</span>;
}
/** 간단 토스트 훅. */
export function useToast() {
const [toast, setToast] = useState<string | null>(null);
const show = useCallback((msg: string) => {
setToast(msg);
window.setTimeout(() => setToast(null), 2600);
}, []);
const node = toast ? (
<div className="kx-toast" role="status">
{toast}
</div>
) : null;
return { show, node };
}

View File

@ -1,418 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX EMS - 서류 및 마일스톤</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #c2c7d1;
border-radius: 10px;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"primary": "#003765",
"surface-tint": "#24609e",
"on-secondary-fixed": "#1b0063",
"inverse-surface": "#293042",
"outline": "#727781",
"on-secondary-fixed-variant": "#4500d8",
"tertiary-container": "#00583b",
"primary-container": "#004e8b",
"on-primary-container": "#91c0ff",
"on-error": "#ffffff",
"primary-fixed": "#d3e4ff",
"on-tertiary-fixed-variant": "#005236",
"surface-container-high": "#e1e7ff",
"on-primary-fixed": "#001c38",
"outline-variant": "#c2c7d1",
"inverse-primary": "#a2c9ff",
"secondary-container": "#6d4aff",
"surface-container": "#eaedff",
"surface-dim": "#d3d9f1",
"error": "#ba1a1a",
"surface": "#faf8ff",
"secondary-fixed-dim": "#c9bfff",
"background": "#faf8ff",
"surface-container-lowest": "#ffffff",
"surface-bright": "#faf8ff",
"tertiary": "#003e28",
"inverse-on-surface": "#eef0ff",
"tertiary-fixed-dim": "#72daa9",
"on-surface-variant": "#424750",
"on-error-container": "#93000a",
"error-container": "#ffdad6",
"error-red": "#ba1a1a",
"surface-container-low": "#f1f3ff",
"secondary-fixed": "#e5deff",
"on-tertiary-container": "#68d1a0",
"on-background": "#141b2c",
"on-surface": "#141b2c",
"secondary": "#5427e6",
"on-tertiary-fixed": "#002113",
"on-secondary-container": "#f3edff",
"on-secondary": "#ffffff",
"on-primary": "#ffffff",
"on-primary-fixed-variant": "#004881",
"on-tertiary": "#ffffff",
"surface-variant": "#dbe2fa",
"tertiary-fixed": "#8ef7c4",
"primary-fixed-dim": "#a2c9ff",
"surface-container-highest": "#dbe2fa"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-sm": "8px",
"stack-md": "16px",
"gutter": "16px",
"stack-lg": "24px",
"section-padding": "96px",
"base": "4px",
"container-padding": "24px"
},
"fontFamily": {
"headline-md": ["Inter", "sans-serif"],
"title-sm": ["Inter", "sans-serif"],
"body-sm": ["Inter", "sans-serif"],
"label-md": ["Geist", "sans-serif"],
"display-lg": ["Inter", "sans-serif"],
"body-base": ["Inter", "sans-serif"]
},
"fontSize": {
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
}
}
}
}
</script>
</head>
<body class="bg-surface-bright text-on-surface font-body-base antialiased">
<!-- Sidebar Navigation -->
<aside class="fixed left-0 top-0 h-screen w-64 border-r border-outline-variant bg-surface-container-lowest flex flex-col p-4 z-50">
<div class="mb-8 px-2">
<h1 class="text-headline-md font-display-lg text-primary tracking-tighter">KINTEX EMS</h1>
<p class="text-label-md text-on-surface-variant">Enterprise Management</p>
</div>
<nav class="flex-1 space-y-1">
<a class="text-on-surface-variant hover:bg-surface-container rounded-xl flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md">Dashboard</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-container rounded-xl flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined">calendar_month</span>
<span class="font-label-md">Exhibition Schedule</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-container rounded-xl flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined">meeting_room</span>
<span class="font-label-md">Hall Operations</span>
</a>
<a class="bg-secondary-container text-on-secondary-container rounded-xl font-bold flex items-center gap-3 px-4 py-3" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">description</span>
<span class="font-label-md">서류·마일스톤</span>
</a>
</nav>
<div class="mt-auto space-y-1">
<button class="w-full bg-primary text-on-primary rounded-xl py-3 px-4 flex items-center justify-center gap-2 mb-6 hover:bg-primary-container transition-all active:scale-95">
<span class="material-symbols-outlined">add</span>
<span class="font-label-md">New Exhibition</span>
</button>
<a class="text-on-surface-variant hover:bg-surface-container rounded-xl flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-md">Settings</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-container rounded-xl flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined">help</span>
<span class="font-label-md">Support</span>
</a>
</div>
</aside>
<!-- Top Navigation Bar -->
<header class="fixed top-0 right-0 w-[calc(100%-16rem)] h-16 border-b border-outline-variant bg-surface-bright/80 backdrop-blur-md z-40 flex items-center justify-between px-6">
<div class="flex items-center gap-2 text-label-md text-on-surface-variant">
<span>Exhibition</span>
<span class="material-symbols-outlined text-[16px]">chevron_right</span>
<span class="text-primary font-semibold">Hall 1</span>
</div>
<div class="flex items-center gap-4">
<div class="relative group">
<span class="material-symbols-outlined p-2 text-on-surface-variant hover:bg-surface-container rounded-full cursor-pointer transition-colors">notifications</span>
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full border-2 border-surface-bright"></span>
</div>
<span class="material-symbols-outlined p-2 text-on-surface-variant hover:bg-surface-container rounded-full cursor-pointer transition-colors">apps</span>
<div class="flex items-center gap-3 pl-4 border-l border-outline-variant">
<div class="text-right">
<p class="text-label-md font-bold text-on-surface">Admin User</p>
<p class="text-[10px] text-on-surface-variant">Administrator</p>
</div>
<div class="w-8 h-8 rounded-full bg-primary-container overflow-hidden ring-2 ring-outline-variant">
<img class="w-full h-full object-cover" data-alt="A professional corporate headshot of a middle-aged South Korean male administrator, wearing a crisp dark navy business suit and a light blue shirt. He has a warm yet authoritative expression, set against a blurred modern architectural background with cool glass tones and soft lighting, reflecting a premium KINTEX office environment." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDL1fcyb7FDfQ06Uo-kHI5mCja5hHnhJfckNR2gUKgAEgeoVF-OoMD39SCNNLFvIfbjKxJ9bVVsASqBVO260VeJkR5Fngxlc-nlBsZU9Jq_qzWqmkIj6meRNlfSRltmxypvIoj3norVeu8OUezAsRlnt0xpJPFLtvI3OPWwup00aGv0ZC0HvSpV6Z5uYkkkUPyAKuvIWOs_KDLCFL0q7ZfVFxDP4PJdxldsYlJpjKWts8WbLCvkr3rISx0_SrpC-EcagosOSGfO8n4"/>
</div>
</div>
</div>
</header>
<!-- Main Dashboard Content -->
<main class="ml-64 pt-16 min-h-screen">
<div class="max-w-[1280px] mx-auto p-8">
<!-- Milestone Progress Section -->
<section class="mb-10">
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-6 shadow-sm">
<div class="flex items-center justify-between mb-6">
<h2 class="font-headline-md text-primary flex items-center gap-2">
<span class="material-symbols-outlined">reorder</span>
전시 마일스톤 현황
</h2>
<span class="text-label-md bg-secondary-fixed text-on-secondary-fixed-variant px-3 py-1 rounded-full font-bold">D-15 일람</span>
</div>
<div class="relative flex items-center justify-between px-4">
<!-- Connecting Line -->
<div class="absolute top-1/2 left-0 w-full h-[2px] bg-outline-variant -translate-y-1/2 z-0"></div>
<div class="absolute top-1/2 left-0 w-[70%] h-[2px] bg-primary -translate-y-1/2 z-0"></div>
<!-- Milestone 1 -->
<div class="relative z-10 flex flex-col items-center text-center">
<div class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center mb-2 shadow-md">
<span class="material-symbols-outlined">check</span>
</div>
<span class="text-label-md font-bold text-primary">D-150 배정</span>
<span class="text-[10px] text-on-surface-variant">완료</span>
</div>
<!-- Milestone 2 -->
<div class="relative z-10 flex flex-col items-center text-center">
<div class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center mb-2 shadow-md">
<span class="material-symbols-outlined">check</span>
</div>
<span class="text-label-md font-bold text-primary">D-30 사전협의</span>
<span class="text-[10px] text-on-surface-variant">완료</span>
</div>
<!-- Milestone 3 -->
<div class="relative z-10 flex flex-col items-center text-center">
<div class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center mb-2 shadow-md">
<span class="material-symbols-outlined">check</span>
</div>
<span class="text-label-md font-bold text-primary">D-25 유틸리티</span>
<span class="text-[10px] text-on-surface-variant">완료</span>
</div>
<!-- Milestone 4 (Active) -->
<div class="relative z-10 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-surface-container-lowest border-4 border-secondary text-secondary flex items-center justify-center mb-2 shadow-lg animate-pulse">
<span class="material-symbols-outlined">pending</span>
</div>
<span class="text-label-md font-bold text-secondary">D-7 신고서류</span>
<span class="text-[10px] text-on-secondary-fixed-variant px-2 py-0.5 bg-secondary-fixed rounded">진행중</span>
</div>
<!-- Milestone 5 -->
<div class="relative z-10 flex flex-col items-center text-center">
<div class="w-10 h-10 rounded-full bg-surface-container-high border-2 border-outline-variant text-outline flex items-center justify-center mb-2">
<span class="material-symbols-outlined">event</span>
</div>
<span class="text-label-md font-bold text-on-surface-variant">D-0 개장</span>
<span class="text-[10px] text-on-surface-variant">대기</span>
</div>
</div>
</div>
</section>
<div class="grid grid-cols-12 gap-8">
<!-- Main Content (Required Documents) -->
<div class="col-span-8">
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-sm">
<div class="p-6 border-b border-outline-variant flex items-center justify-between">
<div>
<h3 class="text-title-sm text-on-surface">Required Documents Checklist</h3>
<p class="text-body-sm text-on-surface-variant">전시회 개최를 위한 필수 서류 목록 및 승인 현황</p>
</div>
<div class="flex items-center gap-3">
<span class="text-label-md text-primary font-bold">4/8 Complete</span>
<div class="w-32 h-2 bg-surface-container rounded-full overflow-hidden">
<div class="h-full bg-primary w-1/2"></div>
</div>
</div>
</div>
<div class="divide-y divide-outline-variant">
<!-- Document Row 1 -->
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low transition-colors group">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-tertiary-container">task_alt</span>
<span class="font-body-base font-semibold text-on-surface">행사운영계획서</span>
</div>
<div class="flex items-center gap-4">
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-tertiary-fixed text-on-tertiary-fixed-variant">승인</span>
<button class="px-4 py-1.5 border border-outline-variant rounded-lg text-label-md hover:bg-surface-container transition-all">[보기]</button>
</div>
</div>
<!-- Document Row 2 -->
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low transition-colors group">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary">cloud_done</span>
<span class="font-body-base font-semibold text-on-surface">부스배치도</span>
</div>
<div class="flex items-center gap-4">
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-primary-fixed text-on-primary-fixed-variant">제출됨</span>
<button class="px-4 py-1.5 border border-primary text-primary rounded-lg text-label-md hover:bg-primary-fixed transition-all">[HWP 생성]</button>
</div>
</div>
<!-- Document Row 3 (D-Day Action) -->
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low transition-colors bg-secondary-fixed/10">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-secondary">edit_note</span>
<span class="font-body-base font-semibold text-on-surface">재해대처계획서</span>
<span class="bg-error text-white text-[10px] px-2 py-0.5 rounded-full font-bold">D-7</span>
</div>
<div class="flex items-center gap-4">
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-surface-container-highest text-on-surface-variant">작성중</span>
<button class="px-4 py-1.5 bg-secondary text-on-secondary rounded-lg text-label-md hover:bg-on-secondary-fixed-variant shadow-sm transition-all">[작성]</button>
</div>
</div>
<!-- Document Row 4 (Rejected) -->
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low transition-colors">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-error">cancel</span>
<span class="font-body-base font-semibold text-on-surface">리깅 구조계산서</span>
</div>
<div class="flex items-center gap-4">
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-error-container text-on-error-container">반려</span>
<button class="px-4 py-1.5 border border-error text-error rounded-lg text-label-md hover:bg-error-container transition-all">[수정]</button>
</div>
</div>
<!-- Remaining Rows (Pending) -->
<div class="p-4 flex items-center justify-between opacity-60">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-outline">description</span>
<span class="font-body-base text-on-surface">방화관리 책임서약서</span>
</div>
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-surface-container-low text-on-surface-variant">준비중</span>
</div>
<div class="p-4 flex items-center justify-between opacity-60">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-outline">description</span>
<span class="font-body-base text-on-surface">주차관리 신청서</span>
</div>
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-surface-container-low text-on-surface-variant">준비중</span>
</div>
<div class="p-4 flex items-center justify-between opacity-60">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-outline">description</span>
<span class="font-body-base text-on-surface">보안요원 배치계획</span>
</div>
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-surface-container-low text-on-surface-variant">준비중</span>
</div>
<div class="p-4 flex items-center justify-between opacity-60">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-outline">description</span>
<span class="font-body-base text-on-surface">위험물 반입신고서</span>
</div>
<span class="px-3 py-1 rounded-full text-label-md font-bold bg-surface-container-low text-on-surface-variant">준비중</span>
</div>
</div>
</div>
</div>
<!-- Side Panel (AI Compliance) -->
<div class="col-span-4 space-y-6">
<div class="bg-surface-container-lowest border-l-4 border-secondary rounded-xl p-6 shadow-md relative overflow-hidden group">
<div class="absolute top-0 right-0 p-4 opacity-5 group-hover:scale-110 transition-transform">
<span class="material-symbols-outlined text-[64px]" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
</div>
<div class="flex items-center gap-2 mb-4 text-secondary">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
<h3 class="font-title-sm">AI 서류 검수</h3>
</div>
<div class="space-y-4 mb-6">
<!-- Warning 1 -->
<div class="flex gap-3 p-3 rounded-lg bg-error-container/20 border border-error-container/30">
<span class="material-symbols-outlined text-error text-[20px]">warning</span>
<div>
<p class="text-label-md font-bold text-on-error-container">데이터 불일치 감지</p>
<p class="text-body-sm text-on-surface-variant leading-tight">부스배치도 부스 수 486 ≠ 운영계획서 510 (불일치)</p>
</div>
</div>
<!-- Warning 2 -->
<div class="flex gap-3 p-3 rounded-lg bg-surface-container border border-outline-variant">
<span class="material-symbols-outlined text-primary text-[20px]">info</span>
<div>
<p class="text-label-md font-bold text-primary">필수요소 누락</p>
<p class="text-body-sm text-on-surface-variant leading-tight">재해대처계획서 필수요소 누락 2건</p>
</div>
</div>
</div>
<div class="bg-secondary-fixed/30 p-4 rounded-xl mb-6">
<div class="flex items-center gap-2 mb-2 text-on-secondary-fixed">
<span class="material-symbols-outlined text-[18px]">article</span>
<span class="text-label-md font-bold">kxwp 제출 안내</span>
</div>
<p class="text-body-sm text-on-surface-variant leading-relaxed">
파일 자동 생성 후 업로드 안내. 작성된 데이터를 기반으로 공문서식 표준을 준수하는 문서를 즉시 생성할 수 있습니다.
</p>
</div>
<button class="w-full py-4 bg-secondary text-on-secondary rounded-xl font-bold flex items-center justify-center gap-3 hover:shadow-lg transition-all active:scale-[0.98]">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
[자동 문서 생성하기]
</button>
</div>
<!-- Statistics Card -->
<div class="bg-primary text-on-primary rounded-xl p-6 shadow-sm relative overflow-hidden">
<div class="absolute inset-0 opacity-10 bg-cover bg-center" data-alt="A macro-photography shot of a high-tech data visualization interface with glowing blue grid patterns and subtle digital particles. The lighting is low-key, emphasizing deep sapphire blues and crisp white digital data points. It represents a sophisticated B2B management system environment." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuB6l59ZoboD1HbO270x0SFw_KE3smIfyVOITJjMMoAkL0xqZstlQlePSvOZ1W7fHtVinJJekyoj6SokcipW-wYz8vrIihVz5d3B4R3gN7orzawNpZXAcmewVEzaIKiKJJCzysoRc7SaRRbae8dsxmqDUFsN59KoT6j95djzLraqjBm4u-r-g9OniUImGWVTsJ2RMTGrD4AN6gioCGn-qTcmHapuN9a4clfp-iiI3DANt2SrU_jvAhwjp_W_fm4ZahMr9fg75MEvTZ0')"></div>
<div class="relative z-10">
<h4 class="text-label-md uppercase tracking-wider mb-2 opacity-80">전체 공정률</h4>
<div class="text-display-lg leading-none mb-4">62.5%</div>
<div class="flex items-center gap-2 text-body-sm text-primary-fixed">
<span class="material-symbols-outlined text-[16px]">trending_up</span>
<span>지난 주 대비 12% 상승</span>
</div>
</div>
</div>
</div>
</div>
<!-- Empty space for footer context -->
<div class="h-32"></div>
</div>
</main>
<script>
// Simple micro-interactions
document.querySelectorAll('button').forEach(button => {
button.addEventListener('mousedown', () => {
button.classList.add('scale-95');
});
button.addEventListener('mouseup', () => {
button.classList.remove('scale-95');
});
});
// Simulating AI scan pulsing effect
const aiCard = document.querySelector('.border-secondary');
setInterval(() => {
aiCard.classList.toggle('ring-2');
aiCard.classList.toggle('ring-secondary/20');
}, 2000);
</script>
</body></html>

View File

@ -1,356 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX Document Studio | 재해대처계획서</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind Configuration -->
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-error-container": "#93000a",
"primary": "#003765",
"inverse-on-surface": "#eef0ff",
"surface-container-highest": "#dbe2fa",
"on-surface": "#141b2c",
"primary-fixed": "#d3e4ff",
"on-error": "#ffffff",
"error": "#ba1a1a",
"surface": "#faf8ff",
"surface-tint": "#24609e",
"inverse-surface": "#293042",
"tertiary": "#003e28",
"on-primary-fixed": "#001c38",
"tertiary-fixed-dim": "#72daa9",
"tertiary-fixed": "#8ef7c4",
"surface-container-low": "#f1f3ff",
"on-secondary-container": "#f3edff",
"inverse-primary": "#a2c9ff",
"on-surface-variant": "#424750",
"on-tertiary-container": "#68d1a0",
"error-red": "#ba1a1a",
"on-primary": "#ffffff",
"on-tertiary-fixed-variant": "#005236",
"secondary-fixed-dim": "#c9bfff",
"background": "#faf8ff",
"on-tertiary": "#ffffff",
"primary-container": "#004e8b",
"surface-variant": "#dbe2fa",
"secondary": "#5427e6",
"outline-variant": "#c2c7d1",
"on-tertiary-fixed": "#002113",
"surface-bright": "#faf8ff",
"secondary-container": "#6d4aff",
"surface-container-lowest": "#ffffff",
"on-primary-container": "#91c0ff",
"surface-container-high": "#e1e7ff",
"secondary-fixed": "#e5deff",
"tertiary-container": "#00583b",
"primary-fixed-dim": "#a2c9ff",
"surface-container": "#eaedff",
"on-primary-fixed-variant": "#004881",
"outline": "#727781",
"on-secondary-fixed-variant": "#4500d8",
"surface-dim": "#d3d9f1",
"on-secondary": "#ffffff",
"error-container": "#ffdad6",
"on-secondary-fixed": "#1b0063",
"on-background": "#141b2c"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"container-padding": "24px",
"base": "4px",
"stack-sm": "8px",
"section-padding": "96px",
"stack-md": "16px",
"stack-lg": "24px",
"gutter": "16px"
},
"fontFamily": {
"label-md": ["Geist"],
"display-lg-mobile": ["Inter"],
"body-sm": ["Inter"],
"headline-md": ["Inter"],
"label-xs": ["Geist"],
"display-lg": ["Inter"],
"body-base": ["Inter"],
"title-sm": ["Inter"]
},
"fontSize": {
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.a4-preview {
width: 210mm;
min-height: 297mm;
padding: 20mm;
margin: 20px auto;
background: white;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.hwp-table {
border-collapse: collapse;
width: 100%;
border: 1.5px solid #000;
}
.hwp-table th, .hwp-table td {
border: 1px solid #000;
padding: 8px;
font-size: 13px;
}
.hwp-table th { background-color: #f2f2f2; font-weight: bold; width: 25%; }
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.scrollbar-hide::-webkit-scrollbar { display: none; }
</style>
</head>
<body class="bg-surface text-on-surface overflow-hidden">
<!-- Top Navigation Bar -->
<header class="flex justify-between items-center w-full px-container-padding h-16 sticky top-0 z-50 bg-surface border-b border-outline-variant shadow-sm">
<div class="flex items-center gap-8">
<div class="font-display-lg text-display-lg font-bold text-primary-container">KINTEX Document Studio</div>
<nav class="hidden md:flex gap-6">
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-base" href="#">Exhibitions</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-base" href="#">Templates</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-base" href="#">History</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-base" href="#">Resources</a>
</nav>
</div>
<div class="flex items-center gap-4">
<div class="relative flex items-center bg-surface-container-low border border-outline-variant rounded-full px-4 py-1.5 w-64">
<span class="material-symbols-outlined text-outline text-[20px]">search</span>
<input class="bg-transparent border-none focus:ring-0 text-body-sm w-full ml-2" placeholder="Search documents..." type="text"/>
</div>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-transform active:scale-95 duration-150">notifications</button>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-transform active:scale-95 duration-150">settings</button>
<div class="w-8 h-8 rounded-full overflow-hidden border border-outline-variant">
<img class="w-full h-full object-cover" data-alt="A professional male user profile avatar with a friendly expression, set against a neutral studio background, suitable for a corporate management application." src="https://lh3.googleusercontent.com/aida-public/AB6AXuA2A_7Gq1_oUQimKdjSYk--Kr4w3tmnei71GWQhJALfLHARlnjoupSih78YI5-QywKlq_RiVTXBsl5gC09OE0fIPBaHsh86H-U7Nw2UIi_IhUr7hXDbD6-eXGgkLMJfrv564oVbmLRiQl2McMqYA02rp3Gv9wAWOgdrNW2yd9KPQo8f-rYrmxd2xGkMzzoSTtewIMfW50iSPs23lBsPJD-Ef0v_Ln3wF_uLxTlHWVdDI9xi1IObhgb4fT1AFOrggsqJxoSGeki40wU"/>
</div>
</div>
</header>
<div class="flex h-[calc(100vh-64px-60px)]">
<!-- Side Navigation Bar -->
<aside class="fixed left-0 top-16 bottom-0 w-64 flex flex-col py-stack-lg z-40 bg-surface-container-low border-r border-outline-variant">
<div class="px-6 mb-8 flex items-center gap-3">
<div class="w-10 h-10 bg-primary-container rounded-lg flex items-center justify-center">
<img class="w-8 h-8 object-contain" data-alt="The official KINTEX corporate identity logo emblem, featuring bold geometric shapes that symbolize modern architecture and global connection, presented in professional blue tones on a clean background." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAFtwSt-s3T6XpKJNWxB8-kHA10OKHil0bfW6nnIlrofpD1XroGD0CHGZgpXEbwvOalXnsE0ruhyJCNRxFGljlRiUIf8xCrGhB19rTQmIBmQkRSdRR8YqeyG4lR8KKo-C71OTXhLgZMF0NBkKOMRFd3h4X0RyQYMjtnRpns8gUGh90QAwtaN4rj3YsFcFzeyOe0pxrhHgwpAozfW4ZXU3sHZx_nRt9CieupBKb-RxNfoM1XEHVYK80zSNhwYSxlYWb7DgNCSvbEagc"/>
</div>
<div>
<h2 class="font-headline-md text-[16px] font-extrabold text-primary-container leading-tight">Organizer Portal</h2>
<p class="font-label-md text-on-surface-variant opacity-70">Exhibition Management</p>
</div>
</div>
<nav class="flex-1 space-y-1">
<a class="text-on-surface-variant hover:bg-surface-variant rounded-full mx-2 px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">Dashboard</span>
</a>
<a class="bg-secondary-container text-on-secondary-container rounded-full mx-2 px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">description</span>
<span class="font-label-md text-label-md">Exhibitor Forms</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-variant rounded-full mx-2 px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">mail</span>
<span class="font-label-md text-label-md">Official Letters</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-variant rounded-full mx-2 px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">verified</span>
<span class="font-label-md text-label-md">Contract Auth</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-variant rounded-full mx-2 px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">psychology</span>
<span class="font-label-md text-label-md">AI Assistant</span>
</a>
</nav>
<div class="px-2 mt-auto border-t border-outline-variant pt-4 space-y-1">
<a class="text-on-surface-variant hover:bg-surface-variant rounded-full px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">help</span>
<span class="font-label-md text-label-md">Help Center</span>
</a>
<a class="text-on-surface-variant hover:bg-surface-variant rounded-full px-4 py-3 flex items-center gap-3 transition-all active:scale-98" href="#">
<span class="material-symbols-outlined">logout</span>
<span class="font-label-md text-label-md">Log Out</span>
</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-64 w-full flex overflow-hidden">
<!-- Left Side: Form Editor -->
<section class="w-1/2 flex flex-col bg-surface border-r border-outline-variant overflow-y-auto scrollbar-hide px-8 py-10">
<div class="mb-8">
<h1 class="font-headline-md text-headline-md text-on-surface mb-2">재해대처계획서 작성</h1>
<p class="font-body-base text-on-surface-variant">행사 성격에 맞는 안전 관리 계획을 수립하세요.</p>
</div>
<button class="w-full flex items-center justify-center gap-2 py-4 px-6 bg-secondary text-on-secondary rounded-xl font-bold mb-10 transition-all hover:bg-secondary-container active:scale-98 shadow-md">
<span class="material-symbols-outlined">auto_awesome</span>
행사 데이터로 자동 채움
</button>
<div class="space-y-4">
<!-- Section 1: Open Accordion -->
<div class="border border-primary rounded-xl overflow-hidden bg-white">
<div class="flex items-center justify-between p-4 bg-primary text-on-primary cursor-pointer">
<h3 class="font-title-sm">1. 행사 개요</h3>
<span class="material-symbols-outlined">expand_less</span>
</div>
<div class="p-6 space-y-6">
<div class="space-y-2">
<label class="font-label-md text-on-surface-variant block">행사명</label>
<input class="w-full border-outline-variant rounded-lg focus:border-primary focus:ring-1 focus:ring-primary text-body-base py-3" type="text" value="2024 글로벌 테크 엑스포"/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="font-label-md text-on-surface-variant block">개최 일시</label>
<div class="relative">
<input class="w-full border-outline-variant rounded-lg focus:border-primary focus:ring-1 focus:ring-primary text-body-base py-3 px-4" type="date" value="2024-11-15"/>
</div>
</div>
<div class="space-y-2">
<label class="font-label-md text-on-surface-variant block">장소</label>
<select class="w-full border-outline-variant rounded-lg focus:border-primary focus:ring-1 focus:ring-primary text-body-base py-3">
<option>제1전시장 1~3홀</option>
<option>제1전시장 4~5홀</option>
<option>제2전시장 7~8홀</option>
<option>제2전시장 9~10홀</option>
</select>
</div>
</div>
<div class="space-y-2">
<label class="font-label-md text-on-surface-variant block">예상 관람객 수</label>
<div class="relative">
<input class="w-full border-outline-variant rounded-lg focus:border-primary focus:ring-1 focus:ring-primary text-body-base py-3 pr-10" type="number" value="15000"/>
<span class="absolute right-4 top-1/2 -translate-y-1/2 font-label-md text-on-surface-variant"></span>
</div>
</div>
</div>
</div>
<!-- Section 2: Closed Accordion -->
<div class="border border-outline-variant rounded-xl overflow-hidden bg-white">
<div class="flex items-center justify-between p-4 cursor-pointer hover:bg-surface-container-low transition-colors">
<h3 class="font-title-sm text-on-surface">2. 안전 관리 담당</h3>
<span class="material-symbols-outlined text-outline">expand_more</span>
</div>
</div>
<!-- Section 3: Closed Accordion -->
<div class="border border-outline-variant rounded-xl overflow-hidden bg-white">
<div class="flex items-center justify-between p-4 cursor-pointer hover:bg-surface-container-low transition-colors">
<h3 class="font-title-sm text-on-surface">3. 비상 연락 체계</h3>
<span class="material-symbols-outlined text-outline">expand_more</span>
</div>
</div>
<!-- Section 4: Closed Accordion -->
<div class="border border-outline-variant rounded-xl overflow-hidden bg-white">
<div class="flex items-center justify-between p-4 cursor-pointer hover:bg-surface-container-low transition-colors">
<h3 class="font-title-sm text-on-surface">4. 위험물 목록</h3>
<span class="material-symbols-outlined text-outline">expand_more</span>
</div>
</div>
</div>
</section>
<!-- Right Side: Live A4 Preview -->
<section class="w-1/2 bg-surface-dim overflow-y-auto p-12 flex flex-col items-center">
<div class="a4-preview relative">
<div class="text-center mb-10">
<h2 class="text-[28px] font-bold border-b-4 border-black inline-block pb-2 px-8">재 해 대 처 계 획 서</h2>
</div>
<table class="hwp-table mb-8">
<tbody><tr>
<th>행사명</th>
<td colspan="3">2024 글로벌 테크 엑스포</td>
</tr>
<tr>
<th>일시</th>
<td>2024. 11. 15. ~ 11. 17.</td>
<th>장소</th>
<td>제1전시장 1~3홀</td>
</tr>
<tr>
<th>주최/주관</th>
<td>KINTEX 테크조직위원회</td>
<th>예상인원</th>
<td>15,000명 (최대 5,000명/일)</td>
</tr>
</tbody></table>
<div class="mb-8">
<h4 class="text-[16px] font-bold mb-4">1. 안전관리 조직체계</h4>
<div class="border border-black p-4 min-h-[150px] text-[13px] leading-relaxed">
행사장 내 안전사고 예방을 위하여 총괄책임자 1인, 현장안전요원 45인을 배치한다. 비상시 인근 소방서 및 경찰서와의 협조 체계를 구축하여 즉각적인 대응이 가능하도록 운영한다. 각 홀별 출입구에 안전 요원을 상주시켜 밀집도를 관리하며, 정기적인 장내 방송을 통해 안전 수칙을 안내한다.
</div>
</div>
<div class="mb-12">
<h4 class="text-[16px] font-bold mb-4">2. 주요 재해대처 방안</h4>
<ul class="list-disc pl-5 text-[13px] space-y-2">
<li>화재 발생 시: 즉시 소방시설 가동 및 안내 방송을 통한 관람객 대피 유도</li>
<li>응급환자 발생 시: 현장 의무실 이송 및 119 구급대 협조 요청</li>
<li>정전 발생 시: 비상 발전기 가동 및 유도등 점등 확인</li>
</ul>
</div>
<div class="absolute bottom-20 right-20 text-center">
<div class="mb-4 text-[14px]">2024년 10월 28일</div>
<div class="flex items-center gap-4">
<span class="text-[16px] font-bold">주식회사 킨텍스 대표이사</span>
<div class="w-16 h-16 border-2 border-red-500 rounded-full flex items-center justify-center text-red-500 font-bold rotate-12 relative">
<span class="text-[12px]">KINTEX<br/>직인</span>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- Sticky Bottom Bar -->
<footer class="fixed bottom-0 left-64 right-0 flex justify-between items-center px-container-padding py-3 z-50 bg-white/80 backdrop-blur-md border-t border-outline-variant shadow-md">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-on-surface-variant text-[18px]">info</span>
<p class="font-body-sm text-body-sm text-on-surface-variant">제출은 kxwp 업로드용 파일 생성 방식입니다</p>
</div>
<div class="flex items-center gap-3">
<button class="px-6 py-2 border border-outline text-on-surface rounded-lg font-bold text-label-md hover:bg-surface-container-low transition-all active:scale-95">임시 저장</button>
<button class="px-6 py-2 border border-outline text-on-surface rounded-lg font-bold text-label-md hover:bg-surface-container-low transition-all active:scale-95">HWP 생성</button>
<button class="px-6 py-2 border border-outline text-on-surface rounded-lg font-bold text-label-md hover:bg-surface-container-low transition-all active:scale-95">PDF 생성</button>
<button class="px-10 py-2 bg-[#0066B3] text-white rounded-lg font-bold text-label-md hover:opacity-90 transition-all active:scale-95 shadow-lg">제 출</button>
</div>
</footer>
<script>
// Simple Micro-interactions
document.querySelectorAll('.cursor-pointer').forEach(item => {
item.addEventListener('click', () => {
// Simplified toggle logic for visual demo
const icon = item.querySelector('.material-symbols-outlined');
if (icon) {
icon.textContent = icon.textContent === 'expand_more' ? 'expand_less' : 'expand_more';
}
});
});
</script>
</body></html>

View File

@ -1,402 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - SCR-25 Logistics Portal</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"primary-fixed": "#d3e4ff",
"outline": "#727781",
"surface-variant": "#dbe2fa",
"on-secondary-fixed-variant": "#4500d8",
"inverse-on-surface": "#eef0ff",
"on-primary-fixed-variant": "#004881",
"secondary-fixed": "#e5deff",
"tertiary-fixed": "#8ef7c4",
"on-tertiary-fixed": "#002113",
"tertiary": "#003e28",
"surface-container": "#eaedff",
"on-secondary-fixed": "#1b0063",
"tertiary-fixed-dim": "#72daa9",
"primary-fixed-dim": "#a2c9ff",
"surface-bright": "#faf8ff",
"on-tertiary": "#ffffff",
"primary": "#003765",
"background": "#faf8ff",
"surface-dim": "#d3d9f1",
"secondary": "#5427e6",
"surface-container-lowest": "#ffffff",
"on-error": "#ffffff",
"inverse-primary": "#a2c9ff",
"surface": "#faf8ff",
"on-tertiary-fixed-variant": "#005236",
"error": "#ba1a1a",
"secondary-container": "#6d4aff",
"surface-container-low": "#f1f3ff",
"on-primary-fixed": "#001c38",
"outline-variant": "#c2c7d1",
"surface-container-highest": "#dbe2fa",
"on-error-container": "#93000a",
"on-primary-container": "#91c0ff",
"on-background": "#141b2c",
"on-tertiary-container": "#68d1a0",
"primary-container": "#004e8b",
"on-secondary-container": "#f3edff",
"on-primary": "#ffffff",
"on-secondary": "#ffffff",
"error-red": "#ba1a1a",
"on-surface": "#141b2c",
"error-container": "#ffdad6",
"surface-container-high": "#e1e7ff",
"tertiary-container": "#00583b",
"on-surface-variant": "#424750",
"secondary-fixed-dim": "#c9bfff",
"surface-tint": "#24609e",
"inverse-surface": "#293042",
"brand-blue": "#0066B3",
"amber-accent": "#FFB000"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"fontFamily": {
"label-xs": ["Geist"],
"label-md": ["Geist"],
"display-lg": ["Inter"],
"headline-md": ["Inter"],
"title-sm": ["Inter"],
"body-sm": ["Inter"],
"body-base": ["Inter"]
},
"fontSize": {
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
}
}
}
}
</script>
<style>
body { font-family: 'Inter', 'Pretendard', sans-serif; }
.grid-header-sticky { position: sticky; left: 0; z-index: 10; background-color: #ffffff; }
.custom-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #c2c7d1; border-radius: 10px; }
</style>
</head>
<body class="bg-surface-bright text-on-background overflow-hidden flex h-screen">
<!-- SideNavBar -->
<aside class="docked left-0 h-full w-64 flex flex-col p-4 space-y-4 bg-surface-bright border-r border-outline-variant">
<div class="flex items-center space-x-3 px-2 mb-4">
<div class="w-10 h-10 bg-primary flex items-center justify-center rounded-lg">
<span class="material-symbols-outlined text-white" style="font-variation-settings: 'FILL' 1;">automation</span>
</div>
<div>
<h1 class="text-title-sm font-headline-md text-primary uppercase tracking-wider">KINTEX AI</h1>
<p class="text-label-xs text-on-surface-variant">Logistics Management</p>
</div>
</div>
<nav class="flex-1 space-y-1">
<a class="flex items-center space-x-3 px-4 py-3 bg-primary-fixed text-on-primary-fixed font-bold rounded-lg transition-all" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">dashboard</span>
<span class="text-body-base font-body-base">Dashboard</span>
</a>
<a class="flex items-center space-x-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low rounded-lg transition-all" href="#">
<span class="material-symbols-outlined">calendar_today</span>
<span class="text-body-base font-body-base">Exhibition Schedule</span>
</a>
<a class="flex items-center space-x-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low rounded-lg transition-all" href="#">
<span class="material-symbols-outlined">warehouse</span>
<span class="text-body-base font-body-base">Hall Operations</span>
</a>
<a class="flex items-center space-x-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low rounded-lg transition-all" href="#">
<span class="material-symbols-outlined">analytics</span>
<span class="text-body-base font-body-base">Business Intelligence</span>
</a>
</nav>
<div class="pt-4 border-t border-outline-variant space-y-1">
<button class="w-full flex items-center justify-center space-x-2 py-3 bg-primary text-white font-bold rounded-lg mb-4 hover:opacity-90 transition-all">
<span class="material-symbols-outlined">add</span>
<span class="text-label-md">New Reservation</span>
</button>
<a class="flex items-center space-x-3 px-4 py-2 text-on-surface-variant hover:bg-surface-container-low rounded-lg" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="text-body-sm">Settings</span>
</a>
<a class="flex items-center space-x-3 px-4 py-2 text-on-surface-variant hover:bg-surface-container-low rounded-lg" href="#">
<span class="material-symbols-outlined">help</span>
<span class="text-body-sm">Support</span>
</a>
</div>
</aside>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- TopNavBar -->
<header class="flex justify-between items-center w-full px-container-padding h-16 bg-surface-container-lowest border-b border-outline-variant shadow-sm z-20">
<div class="flex items-center space-x-6">
<span class="text-headline-md font-headline-md text-primary">Logistics Portal</span>
<div class="relative">
<span class="absolute inset-y-0 left-3 flex items-center text-outline">
<span class="material-symbols-outlined">search</span>
</span>
<input class="pl-10 pr-4 py-2 w-64 bg-surface-container-low border border-outline-variant rounded-full text-body-sm focus:ring-2 focus:ring-primary outline-none" placeholder="예약 번호 또는 차량 조회" type="text"/>
</div>
</div>
<div class="flex items-center space-x-8">
<nav class="hidden md:flex space-x-6">
<a class="text-primary border-b-2 border-primary pb-1 text-label-md font-label-md transition-all" href="#">Operations</a>
<a class="text-on-surface-variant hover:text-primary text-label-md font-label-md transition-all" href="#">Schedules</a>
<a class="text-on-surface-variant hover:text-primary text-label-md font-label-md transition-all" href="#">Fleet</a>
<a class="text-on-surface-variant hover:text-primary text-label-md font-label-md transition-all" href="#">Reports</a>
</nav>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2 px-3 py-1 bg-tertiary-fixed text-on-tertiary-fixed rounded-full">
<div class="w-2 h-2 bg-tertiary-fixed-dim rounded-full animate-pulse"></div>
<span class="text-label-md">System Status</span>
</div>
<button class="p-2 text-on-surface-variant hover:bg-surface-container-low rounded-full transition-all">
<span class="material-symbols-outlined">notifications</span>
</button>
<div class="flex items-center space-x-2 cursor-pointer hover:bg-surface-container-low p-1 rounded-full pr-3">
<img class="w-8 h-8 rounded-full border border-outline-variant" data-alt="A professional corporate profile avatar of a logistics manager in a modern office, soft lighting, 8k resolution, minimalist style with clean blue accents." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDj0QmxsQnQr53Or23K9nTneIJUeZFgCUKJa_ooxKflp1QxysUAm_9Q1Cdgud5HiWb2ajc6x7xDzdUU706_8jUAPaF5YXnxIGjjZyoDyD69lteZJ4c2hGq4EPRKwc7MbybwfpWxyVr50HGPNQHzlslh7qcEzM7shy47aT9_ufvmizCa5xCnYn9N0C11NqVMJKfqXmOJQctXZbeudfs0pxFXFaGSnxVM0LjSv8GOz5BFjTaG8l38QEk8w46xqMxL9EMlR9w3vbcHCyE"/>
<span class="text-label-md font-label-md text-on-surface">Admin Kim</span>
</div>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="flex-1 flex overflow-hidden">
<!-- Left Area (2/3) -->
<section class="w-2/3 flex flex-col p-8 space-y-6 overflow-y-auto custom-scrollbar">
<div class="flex justify-between items-end">
<div class="flex space-x-4">
<div class="space-y-1">
<label class="text-label-xs text-on-surface-variant">행사 선택</label>
<div class="relative">
<select class="appearance-none bg-surface-container-lowest border border-outline-variant rounded-lg px-4 py-2.5 pr-10 text-body-base font-medium min-w-[240px] focus:ring-2 focus:ring-primary outline-none">
<option>2026 서울 모터쇼</option>
<option>2026 국제 물류 산업전</option>
</select>
<span class="absolute right-3 top-3 material-symbols-outlined pointer-events-none">expand_more</span>
</div>
</div>
<div class="space-y-1">
<label class="text-label-xs text-on-surface-variant">홀 선택</label>
<div class="relative">
<select class="appearance-none bg-surface-container-lowest border border-outline-variant rounded-lg px-4 py-2.5 pr-10 text-body-base font-medium min-w-[120px] focus:ring-2 focus:ring-primary outline-none">
<option>홀 7</option>
<option>홀 8</option>
<option>홀 9</option>
</select>
<span class="absolute right-3 top-3 material-symbols-outlined pointer-events-none">expand_more</span>
</div>
</div>
</div>
<div class="flex items-center bg-surface-container-low p-1 rounded-lg">
<button class="px-6 py-2 bg-white text-primary font-bold rounded shadow-sm border border-outline-variant">반입</button>
<button class="px-6 py-2 text-on-surface-variant hover:text-primary transition-all">반출</button>
</div>
</div>
<!-- Scheduling Grid -->
<div class="flex-1 bg-surface-container-lowest rounded-xl border border-outline-variant shadow-sm overflow-hidden flex flex-col">
<div class="flex border-b border-outline-variant bg-surface-container-low">
<div class="w-24 p-4 font-bold text-label-md text-on-surface-variant border-r border-outline-variant flex items-center justify-center">DOCK</div>
<div class="flex-1 grid grid-cols-12">
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">08:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">09:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">10:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">11:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">12:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">13:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">14:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">15:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">16:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">17:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">18:00</div>
<div class="p-4 text-center text-label-md font-bold text-on-surface-variant border-r border-outline-variant last:border-r-0">19:00</div>
</div>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar">
<!-- Rows -->
<div class="flex h-16 border-b border-outline-variant group">
<div class="w-24 p-4 font-bold text-body-sm text-primary flex items-center justify-center border-r border-outline-variant bg-surface-container-lowest">도크 1</div>
<div class="flex-1 grid grid-cols-12">
<div class="col-span-2 p-1 bg-brand-blue/10 border-r border-outline-variant/30 flex items-center">
<div class="w-full h-full bg-brand-blue text-white p-2 rounded text-[10px] flex items-center truncate">
(주)공간디자인 · 5t 트럭
</div>
</div>
<div class="col-span-1 border-r border-outline-variant/30 hover:bg-surface-container-low cursor-pointer transition-all"></div>
<div class="col-span-2 p-1 border-r border-outline-variant/30">
<div class="w-full h-full bg-amber-accent text-on-surface p-2 rounded text-[10px] flex items-center font-bold">
<span class="material-symbols-outlined text-[14px] mr-1">auto_awesome</span>
중량물 우선 (5t 이상)
</div>
</div>
<div class="col-span-7 border-r border-outline-variant/30 last:border-r-0 hover:bg-surface-container-low cursor-pointer transition-all"></div>
</div>
</div>
<div class="flex h-16 border-b border-outline-variant">
<div class="w-24 p-4 font-bold text-body-sm text-primary flex items-center justify-center border-r border-outline-variant">도크 2</div>
<div class="flex-1 grid grid-cols-12 bg-surface-container-lowest">
<div class="col-span-4 border-r border-outline-variant/30 hover:bg-surface-container-low cursor-pointer transition-all"></div>
<div class="col-span-3 p-1 border-r border-outline-variant/30">
<div class="w-full h-full bg-brand-blue text-white p-2 rounded text-[10px] flex items-center truncate">
글로벌부스테크 · 2.5t 트럭
</div>
</div>
<div class="col-span-5 border-r border-outline-variant/30 last:border-r-0 hover:bg-surface-container-low cursor-pointer transition-all"></div>
</div>
</div>
<div class="flex h-16 border-b border-outline-variant">
<div class="w-24 p-4 font-bold text-body-sm text-primary flex items-center justify-center border-r border-outline-variant">도크 3</div>
<div class="flex-1 grid grid-cols-12">
<div class="col-span-1 border-r border-outline-variant/30 hover:bg-surface-container-low cursor-pointer transition-all"></div>
<div class="col-span-2 p-1 bg-brand-blue/10 border-r border-outline-variant/30 flex items-center">
<div class="w-full h-full bg-brand-blue text-white p-2 rounded text-[10px] flex items-center truncate">
비욘드디자인 · 1t 탑차
</div>
</div>
<div class="col-span-9 border-r border-outline-variant/30 last:border-r-0 hover:bg-surface-container-low cursor-pointer transition-all"></div>
</div>
</div>
<div class="flex h-16 border-b border-outline-variant">
<div class="w-24 p-4 font-bold text-body-sm text-primary flex items-center justify-center border-r border-outline-variant">도크 4</div>
<div class="flex-1 grid grid-cols-12">
<div class="col-span-12 border-r border-outline-variant/30 last:border-r-0 hover:bg-surface-container-low cursor-pointer transition-all"></div>
</div>
</div>
<div class="flex h-16 border-b border-outline-variant">
<div class="w-24 p-4 font-bold text-body-sm text-primary flex items-center justify-center border-r border-outline-variant">도크 5</div>
<div class="flex-1 grid grid-cols-12">
<div class="col-span-8 border-r border-outline-variant/30 hover:bg-surface-container-low cursor-pointer transition-all"></div>
<div class="col-span-2 p-1 border-r border-outline-variant/30">
<div class="w-full h-full bg-brand-blue text-white p-2 rounded text-[10px] flex items-center truncate">
네오로지스 · 5t 윙바디
</div>
</div>
<div class="col-span-2 border-r border-outline-variant/30 last:border-r-0 hover:bg-surface-container-low cursor-pointer transition-all"></div>
</div>
</div>
<div class="flex h-16 border-b border-outline-variant">
<div class="w-24 p-4 font-bold text-body-sm text-primary flex items-center justify-center border-r border-outline-variant">도크 6</div>
<div class="flex-1 grid grid-cols-12">
<div class="col-span-12 hover:bg-surface-container-low cursor-pointer transition-all"></div>
</div>
</div>
</div>
</div>
</section>
<!-- Right Area (1/3) -->
<aside class="w-1/3 bg-white border-l border-outline-variant p-8 flex flex-col space-y-8 overflow-y-auto custom-scrollbar">
<section class="space-y-6">
<div class="flex items-center space-x-2 pb-2 border-b-2 border-primary w-fit">
<span class="material-symbols-outlined text-primary">event_available</span>
<h2 class="text-headline-md font-headline-md text-primary">슬롯 예약</h2>
</div>
<div class="grid grid-cols-1 gap-5">
<div class="space-y-2">
<label class="text-label-md font-label-md text-on-surface">차량 번호</label>
<input class="w-full p-3 bg-surface-container-low border border-outline-variant rounded-lg text-body-base outline-none focus:ring-2 focus:ring-primary" placeholder="예: 12가 3456" type="text"/>
</div>
<div class="space-y-2">
<label class="text-label-md font-label-md text-on-surface">차량 중량</label>
<div class="relative">
<select class="w-full p-3 pr-10 appearance-none bg-surface-container-low border border-outline-variant rounded-lg text-body-base outline-none focus:ring-2 focus:ring-primary">
<option>1t</option>
<option>2.5t</option>
<option>5t</option>
<option>11t 이상</option>
</select>
<span class="absolute right-3 top-3.5 material-symbols-outlined pointer-events-none text-outline">expand_more</span>
</div>
</div>
<div class="space-y-2">
<label class="text-label-md font-label-md text-on-surface">반입 품목</label>
<input class="w-full p-3 bg-surface-container-low border border-outline-variant rounded-lg text-body-base outline-none focus:ring-2 focus:ring-primary" type="text" value="전시 부스 자재"/>
</div>
<label class="flex items-center space-x-3 cursor-pointer group">
<div class="relative flex items-center justify-center">
<input class="peer h-6 w-6 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/>
</div>
<span class="text-body-base font-medium">지게차 신청 (유료)</span>
</label>
</div>
<div class="flex items-center gap-4 bg-primary text-white p-4 rounded-xl shadow-lg cursor-pointer hover:bg-primary-container transition-all group">
<div class="flex-1">
<span class="text-headline-md font-bold block">통행증 발급 (QR)</span>
<span class="text-label-xs opacity-80 uppercase">Generate Logistics Pass</span>
</div>
<div class="bg-white p-1 rounded-lg">
<img class="w-12 h-12" data-alt="A clean, minimalist QR code thumbnail on a white rounded square background, high-tech and sharp vector style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDaJ_Va3nXPp97SeM1apMuvq7RxYcH88bw4dLxx9So1hXylg2QuuRyurp1s5-htuMVgcBRRQvjhThNX6C47Qx44y9aAjlSX2LmBnkQZwrWVMbSBd6wE5TT5B-0VxIzBaTf4nte4Y8fOiWcw5zL3ekPWq-5zsr3FqO8dVLYl6tsFamf11E4RvCeHx8koWXsa-gjnS-xBjlPvrzKUhWIUPchmDk-PfDeJIcM2fP8t5JN74Hhu5Q0X_EXl29ntsdT2ADC3zubTEUGhVSU"/>
</div>
</div>
</section>
<!-- AI Section -->
<section class="bg-surface-container-low rounded-2xl p-6 border border-outline-variant space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="material-symbols-outlined text-secondary" style="font-variation-settings: 'FILL' 1;">psychology</span>
<h3 class="text-title-sm font-headline-md text-on-surface">철거일 대기열 예측 (AI)</h3>
</div>
<span class="text-[10px] font-bold text-secondary-container bg-secondary/10 px-2 py-0.5 rounded-full">REAL-TIME</span>
</div>
<!-- Mini Bar Chart -->
<div class="h-24 flex items-end justify-between px-2 pt-2 border-b border-outline-variant/30">
<div class="w-3 bg-secondary/20 rounded-t h-[20%]"></div>
<div class="w-3 bg-secondary/20 rounded-t h-[30%]"></div>
<div class="w-3 bg-secondary/20 rounded-t h-[40%]"></div>
<div class="w-3 bg-secondary/60 rounded-t h-[70%]"></div>
<div class="w-3 bg-secondary rounded-t h-[95%]"></div>
<div class="w-3 bg-secondary rounded-t h-[100%]"></div>
<div class="w-3 bg-secondary/60 rounded-t h-[80%]"></div>
<div class="w-3 bg-secondary/30 rounded-t h-[50%]"></div>
<div class="w-3 bg-secondary/20 rounded-t h-[30%]"></div>
<div class="w-3 bg-secondary/20 rounded-t h-[20%]"></div>
</div>
<div class="flex justify-between px-1 text-[9px] text-outline">
<span>08:00</span>
<span>12:00</span>
<span>16:00</span>
<span>20:00</span>
</div>
<div class="bg-white/60 backdrop-blur p-4 rounded-lg border-l-4 border-secondary flex space-x-3 items-start">
<span class="material-symbols-outlined text-secondary text-sm">info</span>
<p class="text-body-sm leading-relaxed text-on-surface">
철거 당일 <span class="font-bold text-secondary">오후 4시경</span> 차량 밀집도가 매우 높을 것으로 예상됩니다. 예약 분산을 권장합니다.
</p>
</div>
</section>
</aside>
</main>
</div>
<script>
// Simple interactivity for the toggle switch
document.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', function() {
if(this.parentElement.classList.contains('bg-surface-container-low')) {
this.parentElement.querySelectorAll('button').forEach(b => {
b.className = 'px-6 py-2 text-on-surface-variant hover:text-primary transition-all';
});
this.className = 'px-6 py-2 bg-white text-primary font-bold rounded shadow-sm border border-outline-variant';
}
});
});
</script>
</body></html>

View File

@ -1,474 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>SCR-26 Construction Auction List | KINTEX AI System</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
body {
font-family: 'Inter', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
background-color: #faf8ff;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #dbe2fa;
border-radius: 10px;
}
.accent-border-primary { border-left: 4px solid #003765; }
.accent-border-secondary { border-left: 4px solid #5427e6; }
.accent-border-tertiary { border-left: 4px solid #00583b; }
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-background": "#141b2c",
"surface-container": "#eaedff",
"secondary-fixed": "#e5deff",
"primary-container": "#004e8b",
"primary": "#003765",
"on-secondary-fixed-variant": "#4500d8",
"secondary": "#5427e6",
"inverse-on-surface": "#eef0ff",
"on-primary-fixed": "#001c38",
"surface-bright": "#faf8ff",
"surface": "#faf8ff",
"on-tertiary-container": "#68d1a0",
"on-primary-fixed-variant": "#004881",
"on-error": "#ffffff",
"primary-fixed": "#d3e4ff",
"on-tertiary-fixed-variant": "#005236",
"surface-variant": "#dbe2fa",
"tertiary": "#003e28",
"surface-container-lowest": "#ffffff",
"outline-variant": "#c2c7d1",
"surface-container-low": "#f2f3ff",
"on-surface-variant": "#424750",
"tertiary-container": "#00583b",
"secondary-fixed-dim": "#c9bfff",
"secondary-container": "#6d4aff",
"on-secondary-container": "#f3edff",
"outline": "#727781",
"tertiary-fixed-dim": "#72daa9",
"surface-container-highest": "#dbe2fa",
"primary-fixed-dim": "#a2c9ff",
"on-primary-container": "#91c0ff",
"surface-tint": "#24609e",
"error-container": "#ffdad6",
"on-secondary": "#ffffff",
"inverse-primary": "#a2c9ff",
"error": "#ba1a1a",
"on-error-container": "#93000a",
"surface-container-high": "#e1e7ff",
"on-secondary-fixed": "#1b0063",
"on-surface": "#141b2c",
"inverse-surface": "#293042",
"on-tertiary": "#ffffff",
"error-red": "#ba1a1a",
"tertiary-fixed": "#8ef7c4",
"background": "#faf8ff",
"on-primary": "#ffffff",
"surface-dim": "#d3d9f1",
"on-tertiary-fixed": "#002113"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-md": "16px",
"gutter": "16px",
"stack-lg": "24px",
"stack-sm": "8px",
"container-padding": "24px",
"base": "4px",
"section-padding": "96px"
},
"fontFamily": {
"label-xs": ["Geist"],
"display-lg": ["Inter"],
"title-sm": ["Inter"],
"headline-md": ["Inter"],
"body-sm": ["Inter"],
"body-base": ["Inter"],
"label-md": ["Geist"]
}
}
}
}
</script>
</head>
<body class="bg-surface text-on-surface">
<!-- Top Navigation Bar -->
<header class="flex justify-between items-center h-16 px-gutter w-full sticky top-0 z-50 bg-surface border-b border-outline-variant">
<div class="flex items-center gap-stack-lg">
<span class="text-headline-md font-headline-md font-bold text-primary">KINTEX AI System</span>
<div class="h-6 w-px bg-outline-variant hidden md:block"></div>
<h1 class="text-title-sm font-title-sm text-on-surface-variant">옥션 관리 (SCR-26)</h1>
</div>
<div class="flex items-center gap-stack-md">
<nav class="hidden md:flex gap-stack-lg mr-stack-lg">
<a class="text-label-md font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Auctions</a>
<a class="text-label-md font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Exhibitors</a>
<a class="text-label-md font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Resources</a>
</nav>
<div class="flex items-center gap-2">
<button class="p-2 text-on-surface-variant hover:bg-surface-container-low transition-colors rounded-full">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="p-2 text-on-surface-variant hover:bg-surface-container-low transition-colors rounded-full">
<span class="material-symbols-outlined">settings</span>
</button>
<button class="flex items-center gap-2 px-3 py-1.5 rounded-full border border-outline-variant hover:bg-surface-container-low transition-colors">
<span class="material-symbols-outlined">account_circle</span>
<span class="text-label-md font-label-md">Admin</span>
</button>
</div>
<button class="ml-4 bg-primary text-on-primary px-6 py-2 rounded-full text-label-md font-label-md font-bold hover:bg-primary-container transition-opacity">Live Auction</button>
</div>
</header>
<div class="flex min-h-[calc(100vh-64px)]">
<!-- Side Navigation Bar -->
<aside class="hidden md:flex flex-col h-[calc(100vh-64px)] py-stack-md bg-surface-container-low border-r border-outline-variant w-64 sticky top-16">
<div class="px-gutter mb-stack-lg">
<div class="flex items-center gap-3 p-3 bg-surface-container-lowest rounded-xl border border-outline-variant">
<div class="w-10 h-10 rounded-full bg-primary-fixed flex items-center justify-center text-primary">
<span class="material-symbols-outlined">person</span>
</div>
<div>
<p class="text-label-md font-label-md font-bold text-on-surface">Admin Console</p>
<p class="text-body-sm font-body-sm text-on-surface-variant">Exhibition Manager</p>
</div>
</div>
</div>
<nav class="flex-1 space-y-1">
<a class="flex items-center gap-3 px-gutter py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="text-label-md font-label-md">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-gutter py-3 bg-secondary-fixed text-on-secondary-fixed rounded-r-full font-bold" href="#">
<span class="material-symbols-outlined">gavel</span>
<span class="text-label-md font-label-md">Auction</span>
</a>
<a class="flex items-center gap-3 px-gutter py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">calendar_today</span>
<span class="text-label-md font-label-md">Exhibition Schedule</span>
</a>
<a class="flex items-center gap-3 px-gutter py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">domain</span>
<span class="text-label-md font-label-md">Hall Operations</span>
</a>
<a class="flex items-center gap-3 px-gutter py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">analytics</span>
<span class="text-label-md font-label-md">Reports</span>
</a>
</nav>
<div class="px-gutter mt-auto pt-stack-md border-t border-outline-variant/30">
<a class="flex items-center gap-3 py-2 text-on-surface-variant hover:text-primary" href="#">
<span class="material-symbols-outlined text-[20px]">help</span>
<span class="text-label-md font-label-md">Support</span>
</a>
<a class="flex items-center gap-3 py-2 text-on-surface-variant hover:text-primary" href="#">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span class="text-label-md font-label-md">Settings</span>
</a>
<button class="w-full mt-4 bg-secondary text-on-secondary py-3 rounded-xl font-bold text-label-md hover:bg-secondary-container transition-all flex items-center justify-center gap-2">
<span class="material-symbols-outlined">add_circle</span>
New Auction
</button>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 p-gutter max-w-[1440px] mx-auto grid grid-cols-12 gap-gutter">
<!-- List Section (Left Column) -->
<div class="col-span-12 lg:col-span-8 space-y-stack-lg">
<!-- Filter Tabs & Create Button -->
<div class="flex items-center justify-between bg-surface-container-lowest p-stack-sm rounded-xl border border-outline-variant shadow-sm">
<div class="flex gap-2 p-1 bg-surface-container-low rounded-lg">
<button class="px-6 py-2 rounded-md bg-surface-container-lowest text-primary font-bold shadow-sm text-label-md">진행중</button>
<button class="px-6 py-2 rounded-md text-on-surface-variant hover:bg-surface-container-high text-label-md transition-colors">마감</button>
<button class="px-6 py-2 rounded-md text-on-surface-variant hover:bg-surface-container-high text-label-md transition-colors">내가 개설</button>
</div>
<button class="flex items-center gap-2 bg-primary text-on-primary px-6 py-2.5 rounded-lg font-bold text-label-md hover:shadow-lg transition-all">
<span class="material-symbols-outlined">add</span>
옥션 개설
</button>
</div>
<!-- Auction Cards List -->
<div class="space-y-stack-md">
<!-- Card 1 -->
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden hover:shadow-md transition-all accent-border-primary">
<div class="p-gutter">
<div class="flex justify-between items-start mb-4">
<div class="space-y-2">
<div class="flex items-center gap-2">
<span class="px-2.5 py-0.5 bg-primary-fixed text-on-primary-fixed rounded-full text-label-xs font-label-xs">전시디자인설치</span>
<span class="px-2.5 py-0.5 bg-secondary-fixed text-on-secondary-fixed rounded-full text-label-xs font-label-xs">역경매</span>
</div>
<h3 class="text-headline-md font-headline-md text-on-surface">A-102 독립부스 시공</h3>
</div>
<div class="flex flex-col items-end gap-2">
<span class="px-3 py-1 bg-error text-on-error rounded-md text-label-md font-bold">마감 D-2</span>
<span class="px-3 py-1 bg-tertiary-fixed text-on-tertiary-fixed rounded-md text-label-md">진행중</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 py-4 border-y border-outline-variant/30 mb-4">
<div class="flex flex-col">
<span class="text-body-sm font-body-sm text-on-surface-variant">응찰 현황</span>
<span class="text-title-sm font-title-sm font-bold text-on-surface">5개 업체 응찰 중</span>
</div>
<div class="flex flex-col items-end">
<span class="text-body-sm font-body-sm text-on-surface-variant">현재 최저가</span>
<span class="text-headline-md font-headline-md font-bold text-primary">₩8,400,000</span>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex gap-4">
<div class="flex items-center gap-1.5 text-secondary">
<span class="material-symbols-outlined text-[20px]">layers</span>
<span class="text-label-md font-label-md">배치 (Layout)</span>
</div>
<div class="flex items-center gap-1.5 text-secondary">
<span class="material-symbols-outlined text-[20px]">architecture</span>
<span class="text-label-md font-label-md">설계 (Design)</span>
</div>
<div class="flex items-center gap-1.5 text-secondary">
<span class="material-symbols-outlined text-[20px]">description</span>
<span class="text-label-md font-label-md">물량서 (BOQ)</span>
</div>
<div class="flex items-center gap-1.5 text-secondary">
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
<span class="text-label-md font-label-md font-bold">AI Preview</span>
</div>
</div>
<button class="px-4 py-2 border border-primary text-primary rounded-lg font-bold text-label-md hover:bg-primary-fixed transition-colors">상세보기</button>
</div>
</div>
</div>
<!-- Card 2 -->
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden hover:shadow-md transition-all accent-border-secondary">
<div class="p-gutter">
<div class="flex justify-between items-start mb-4">
<div class="space-y-2">
<div class="flex items-center gap-2">
<span class="px-2.5 py-0.5 bg-primary-fixed text-on-primary-fixed rounded-full text-label-xs font-label-xs">구조물 임대</span>
<span class="px-2.5 py-0.5 bg-secondary-fixed text-on-secondary-fixed rounded-full text-label-xs font-label-xs">역경매</span>
</div>
<h3 class="text-headline-md font-headline-md text-on-surface">B-405 트러스 구조물 설치</h3>
</div>
<div class="flex flex-col items-end gap-2">
<span class="px-3 py-1 bg-error text-on-error rounded-md text-label-md font-bold">마감 D-1</span>
<span class="px-3 py-1 bg-tertiary-fixed text-on-tertiary-fixed rounded-md text-label-md">진행중</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 py-4 border-y border-outline-variant/30 mb-4">
<div class="flex flex-col">
<span class="text-body-sm font-body-sm text-on-surface-variant">응찰 현황</span>
<span class="text-title-sm font-title-sm font-bold text-on-surface">12개 업체 응찰 중</span>
</div>
<div class="flex flex-col items-end">
<span class="text-body-sm font-body-sm text-on-surface-variant">현재 최저가</span>
<span class="text-headline-md font-headline-md font-bold text-primary">₩4,250,000</span>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex gap-4">
<div class="flex items-center gap-1.5 text-secondary">
<span class="material-symbols-outlined text-[20px]">layers</span>
<span class="text-label-md font-label-md">배치</span>
</div>
<div class="flex items-center gap-1.5 text-secondary">
<span class="material-symbols-outlined text-[20px]">description</span>
<span class="text-label-md font-label-md">물량서</span>
</div>
</div>
<button class="px-4 py-2 border border-primary text-primary rounded-lg font-bold text-label-md hover:bg-primary-fixed transition-colors">상세보기</button>
</div>
</div>
</div>
<!-- Card 3 -->
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden hover:shadow-md transition-all accent-border-tertiary opacity-80">
<div class="p-gutter">
<div class="flex justify-between items-start mb-4">
<div class="space-y-2">
<div class="flex items-center gap-2">
<span class="px-2.5 py-0.5 bg-surface-variant text-on-surface-variant rounded-full text-label-xs font-label-xs">전기/조명</span>
<span class="px-2.5 py-0.5 bg-surface-variant text-on-surface-variant rounded-full text-label-xs font-label-xs">역경매</span>
</div>
<h3 class="text-headline-md font-headline-md text-on-surface">C-10 공용 통로 조명 공사</h3>
</div>
<div class="flex flex-col items-end gap-2">
<span class="px-3 py-1 bg-surface-variant text-on-surface-variant rounded-md text-label-md font-bold">마감 완료</span>
<span class="px-3 py-1 bg-surface-container-high text-on-surface-variant rounded-md text-label-md">정산 중</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 py-4 border-y border-outline-variant/30 mb-4">
<div class="flex flex-col">
<span class="text-body-sm font-body-sm text-on-surface-variant">낙찰 업체</span>
<span class="text-title-sm font-title-sm font-bold text-on-surface">(주)테크라이팅</span>
</div>
<div class="flex flex-col items-end">
<span class="text-body-sm font-body-sm text-on-surface-variant">최종 낙찰가</span>
<span class="text-headline-md font-headline-md font-bold text-on-surface-variant">₩12,000,000</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar (Right Column) -->
<div class="col-span-12 lg:col-span-4">
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant shadow-lg sticky top-24 overflow-hidden">
<div class="p-stack-lg bg-primary text-on-primary">
<h2 class="text-title-sm font-title-sm font-bold flex items-center gap-2">
<span class="material-symbols-outlined">add_box</span>
옥션 개설 (Open Auction)
</h2>
<p class="text-body-sm font-body-sm opacity-90 mt-1">새로운 시공 옥션을 공고하고 협력사를 모집합니다.</p>
</div>
<div class="p-gutter space-y-stack-lg custom-scrollbar max-h-[716px] overflow-y-auto">
<!-- Basic Info -->
<div class="space-y-stack-sm">
<label class="text-label-md font-label-md text-on-surface-variant">카테고리 선택</label>
<select class="w-full border-outline-variant rounded-lg bg-surface-container-low text-body-base focus:ring-primary focus:border-primary">
<option>전시디자인설치</option>
<option>구조물 임대</option>
<option>전기/조명</option>
<option>영상/음향</option>
</select>
</div>
<!-- Award Criteria -->
<div class="space-y-stack-sm">
<label class="text-label-md font-label-md text-on-surface-variant">낙찰 기준 (Award Criteria)</label>
<div class="flex p-1 bg-surface-container-low rounded-lg border border-outline-variant">
<button class="flex-1 py-2 text-label-md rounded-md transition-all hover:bg-surface-container-high">최저가</button>
<button class="flex-1 py-2 text-label-md rounded-md bg-white shadow-sm font-bold text-primary">종합평가</button>
</div>
</div>
<!-- Weights (Visible for Comprehensive) -->
<div class="p-stack-md bg-secondary-fixed/30 rounded-xl space-y-4 border border-secondary-fixed">
<div class="space-y-1">
<div class="flex justify-between text-label-xs font-label-xs text-on-secondary-fixed-variant">
<span>가격 평가</span>
<span class="font-bold">60%</span>
</div>
<input class="w-full h-1.5 bg-surface-container-high rounded-lg appearance-none cursor-pointer accent-secondary" type="range" value="60"/>
</div>
<div class="space-y-1">
<div class="flex justify-between text-label-xs font-label-xs text-on-secondary-fixed-variant">
<span>평판/경력</span>
<span class="font-bold">25%</span>
</div>
<input class="w-full h-1.5 bg-surface-container-high rounded-lg appearance-none cursor-pointer accent-secondary" type="range" value="25"/>
</div>
<div class="space-y-1">
<div class="flex justify-between text-label-xs font-label-xs text-on-secondary-fixed-variant">
<span>납기/준수</span>
<span class="font-bold">15%</span>
</div>
<input class="w-full h-1.5 bg-surface-container-high rounded-lg appearance-none cursor-pointer accent-secondary" type="range" value="15"/>
</div>
</div>
<!-- Round & Deadline -->
<div class="grid grid-cols-2 gap-stack-md">
<div class="space-y-stack-sm">
<label class="text-label-md font-label-md text-on-surface-variant">차수 (Round)</label>
<input class="w-full border-outline-variant rounded-lg bg-surface-container-low text-body-base" type="text" value="1차"/>
</div>
<div class="space-y-stack-sm">
<label class="text-label-md font-label-md text-on-surface-variant">마감 기한</label>
<input class="w-full border-outline-variant rounded-lg bg-surface-container-low text-body-base" type="date"/>
</div>
</div>
<!-- AI Attachment Picker -->
<div class="space-y-stack-sm">
<label class="text-label-md font-label-md text-on-surface-variant">설계 데이터 연동</label>
<button class="w-full flex flex-col items-center justify-center p-6 border-2 border-dashed border-secondary-container bg-secondary-container/5 rounded-xl hover:bg-secondary-container/10 transition-all group">
<span class="material-symbols-outlined text-[32px] text-secondary mb-2 group-hover:scale-110 transition-transform" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
<span class="text-label-md font-bold text-secondary">M2~M5 자료 첨부</span>
<span class="text-label-xs text-on-surface-variant mt-1">도면, BOQ, AI 렌더링 포함</span>
</button>
</div>
<!-- Contractor Invitation -->
<div class="space-y-stack-sm">
<label class="text-label-md font-label-md text-on-surface-variant flex justify-between">
협력사 초대
<span class="text-label-xs text-error font-normal">등록업체만 초대 가능</span>
</label>
<div class="relative">
<input class="w-full pl-10 border-outline-variant rounded-lg bg-surface-container-low text-body-base" placeholder="업체명 검색..." type="text"/>
<span class="material-symbols-outlined absolute left-3 top-2.5 text-on-surface-variant text-[20px]">search</span>
</div>
<div class="max-h-32 overflow-y-auto border border-outline-variant rounded-lg divide-y divide-outline-variant/30">
<div class="p-2 flex items-center justify-between hover:bg-surface-container-low">
<span class="text-body-sm">(주)에이치디자인</span>
<input checked="" class="rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/>
</div>
<div class="p-2 flex items-center justify-between hover:bg-surface-container-low">
<span class="text-body-sm">(주)공간창작</span>
<input class="rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/>
</div>
<div class="p-2 flex items-center justify-between hover:bg-surface-container-low">
<span class="text-body-sm">K-전시산업</span>
<input class="rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/>
</div>
</div>
</div>
</div>
<!-- Final CTA -->
<div class="p-gutter border-t border-outline-variant bg-surface-container-low">
<button class="w-full bg-primary text-on-primary py-4 rounded-xl font-bold text-title-sm hover:bg-primary-container transition-all shadow-md active:scale-[0.98]">
옥션 공고 게시
</button>
</div>
</div>
</div>
</main>
</div>
<!-- Floating Tooltip / Micro-interaction layer -->
<div class="fixed bottom-8 right-8 flex items-center gap-3 px-4 py-3 bg-on-surface text-surface rounded-full shadow-2xl transition-all translate-y-20 opacity-0" id="ai-status-indicator">
<span class="material-symbols-outlined text-secondary" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
<span class="text-label-md font-label-md">AI 분석: 현재 응찰가는 시장 평균 대비 5% 낮음</span>
</div>
<script>
// Simple entrance animation for the AI indicator
window.addEventListener('load', () => {
setTimeout(() => {
const indicator = document.getElementById('ai-status-indicator');
indicator.classList.remove('translate-y-20', 'opacity-0');
}, 1000);
});
// Hover effect for auction cards
document.querySelectorAll('.accent-border-primary, .accent-border-secondary').forEach(card => {
card.addEventListener('mouseenter', () => {
card.classList.add('shadow-xl', '-translate-y-1');
});
card.addEventListener('mouseleave', () => {
card.classList.remove('shadow-xl', '-translate-y-1');
});
});
</script>
</body></html>

View File

@ -1,370 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI System - Auction Detail</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-surface-variant": "#424750",
"inverse-on-surface": "#eef0ff",
"surface-bright": "#f9f9ff",
"primary-fixed": "#d3e4ff",
"on-surface": "#141b2c",
"outline-variant": "#c2c7d1",
"surface-container-lowest": "#ffffff",
"error-container": "#ffdad6",
"on-tertiary-container": "#68d1a0",
"on-secondary-fixed": "#1b0063",
"inverse-primary": "#a2c9ff",
"on-error": "#ffffff",
"tertiary": "#003e28",
"outline": "#727781",
"inverse-surface": "#293042",
"surface-dim": "#d3d9f1",
"on-background": "#141b2c",
"primary": "#003765",
"tertiary-fixed-dim": "#72daa9",
"surface-container-highest": "#dbe2fa",
"on-tertiary-fixed": "#002113",
"on-secondary": "#ffffff",
"on-tertiary": "#ffffff",
"primary-container": "#004e8b",
"on-primary-container": "#91c0ff",
"tertiary-container": "#00583b",
"tertiary-fixed": "#8ef7c4",
"on-tertiary-fixed-variant": "#005236",
"surface-container-low": "#f2f3ff",
"secondary": "#5427e6",
"surface": "#faf8ff",
"secondary-fixed": "#e5deff",
"secondary-container": "#6d4aff",
"on-primary-fixed-variant": "#004881",
"secondary-fixed-dim": "#c9bfff",
"surface-container-high": "#e1e7ff",
"on-error-container": "#93000a",
"error-red": "#ba1a1a",
"on-primary-fixed": "#001c38",
"on-secondary-container": "#f3edff",
"surface-container": "#eaedff",
"primary-fixed-dim": "#a2c9ff",
"on-primary": "#ffffff",
"on-secondary-fixed-variant": "#4500d8",
"background": "#faf8ff",
"surface-variant": "#dbe2fa",
"surface-tint": "#24609e",
"error": "#ba1a1a"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"gutter": "16px",
"base": "4px",
"stack-sm": "8px",
"container-padding": "24px",
"stack-lg": "24px",
"stack-md": "16px",
"section-padding": "96px"
},
"fontFamily": {
"title-sm": ["Inter"],
"label-md": ["Geist"],
"headline-md": ["Inter"],
"body-sm": ["Inter"],
"display-lg": ["Inter"],
"body-base": ["Inter"],
"label-xs": ["Geist"]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 10px;
}
.glass-overlay {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.active-tab-indicator {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #003765;
}
</style>
</head>
<body class="bg-surface-bright text-on-surface font-body-base antialiased">
<!-- TopNavBar (Shared Component) -->
<nav class="flex justify-between items-center h-16 px-gutter w-full sticky top-0 z-50 bg-surface border-b border-outline-variant">
<div class="flex items-center gap-8">
<span class="text-headline-md font-headline-md font-bold text-primary">KINTEX AI System</span>
<div class="hidden md:flex items-center gap-6">
<a class="text-primary border-b-2 border-primary pb-1 font-label-md text-label-md" href="#">Auctions</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-label-md text-label-md" href="#">Exhibitors</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-label-md text-label-md" href="#">Resources</a>
</div>
</div>
<div class="flex items-center gap-4">
<div class="relative flex items-center bg-surface-container-low rounded-full px-4 py-1.5 border border-outline-variant">
<span class="material-symbols-outlined text-on-surface-variant text-[20px]">search</span>
<input class="bg-transparent border-none focus:ring-0 text-sm w-48 ml-2" placeholder="검색" type="text"/>
</div>
<div class="flex items-center gap-2">
<button class="p-2 hover:bg-surface-container-low rounded-full transition-colors">
<span class="material-symbols-outlined text-on-surface-variant">notifications</span>
</button>
<button class="p-2 hover:bg-surface-container-low rounded-full transition-colors">
<span class="material-symbols-outlined text-on-surface-variant">settings</span>
</button>
<button class="p-2 hover:bg-surface-container-low rounded-full transition-colors">
<span class="material-symbols-outlined text-on-surface-variant">account_circle</span>
</button>
</div>
<button class="ml-2 px-6 py-2 bg-primary text-white font-label-md text-label-md rounded-full hover:bg-primary-container transition-all active:opacity-80">
Live Auction
</button>
</div>
</nav>
<div class="flex h-[calc(100vh-64px)] overflow-hidden">
<!-- SideNavBar (Shared Component) -->
<aside class="w-64 bg-surface-container-low border-r border-outline-variant flex flex-col py-stack-md shrink-0">
<div class="px-6 mb-8">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-surface-container-highest flex items-center justify-center border border-outline-variant overflow-hidden">
<img class="w-full h-full object-cover" data-alt="A professional corporate headshot of a business manager wearing a dark suit, set against a clean, softly lit studio background. The style is modern and authoritative, with neutral lighting and a high-end professional aesthetic." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBs8MrUZOIgnGLPxnHqFP8UV37C_96Jl5YRx9lM7D-YxvMsHCwFSE6i70M2NvCBa5r64WGoWx7g-QknzfvFVCRHz_OUlvqHcF7EaqxaFLuCAbT9Py0i4jyxa2A_5QGXJigyW3QamPog3unhnYRl7R3uw1Q1Pe6qu9wMo0YjYsilU4TwibWHZ6xmkpMwAPuT1m-CAzox4ZxFMCMaNp-y-kQURZov6z2rnK5EF-ClUWhCXAfsqI6ORLmwpvHAvlsW773l3x2RjJ5cGGM"/>
</div>
<div>
<p class="text-title-sm font-title-sm text-on-surface">Admin Console</p>
<p class="text-body-sm font-body-sm text-on-surface-variant">Exhibition Manager</p>
</div>
</div>
</div>
<nav class="flex-1 space-y-1">
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="text-label-md font-label-md">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-6 py-3 bg-secondary-fixed text-on-secondary-fixed rounded-r-full font-bold" href="#">
<span class="material-symbols-outlined">calendar_today</span>
<span class="text-label-md font-label-md">Exhibition Schedule</span>
</a>
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">domain</span>
<span class="text-label-md font-label-md">Hall Operations</span>
</a>
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">gavel</span>
<span class="text-label-md font-label-md">Bidding History</span>
</a>
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined">analytics</span>
<span class="text-label-md font-label-md">Reports</span>
</a>
</nav>
<div class="px-4 mt-auto mb-6">
<button class="w-full py-3 bg-primary text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-primary-container transition-transform active:scale-95 shadow-md">
<span class="material-symbols-outlined text-[20px]">add</span>
New Auction
</button>
</div>
<div class="pt-4 border-t border-outline-variant">
<a class="flex items-center gap-3 px-6 py-2 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined text-[20px]">help</span>
<span class="text-label-md font-label-md">Support</span>
</a>
<a class="flex items-center gap-3 px-6 py-2 text-on-surface-variant hover:bg-surface-container-high transition-transform duration-200 hover:translate-x-1" href="#">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span class="text-label-md font-label-md">Settings</span>
</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 flex flex-col min-w-0 bg-surface-bright">
<!-- Auction Header Section -->
<header class="bg-surface-container-lowest border-b border-outline-variant px-container-padding h-20 flex items-center justify-between">
<div class="flex items-center gap-4">
<span class="bg-error-red text-white px-3 py-1 rounded text-label-xs font-bold animate-pulse">실시간</span>
<h1 class="text-headline-md font-headline-md text-on-surface">A-102 독립부스 시공 | 역경매 | 라운드 2</h1>
</div>
<div class="flex items-center gap-6">
<div class="flex flex-col items-end">
<span class="text-label-xs text-on-surface-variant uppercase tracking-wider">상태</span>
<span class="text-label-md font-bold text-tertiary">활성 응찰 중</span>
</div>
<div class="h-8 w-px bg-outline-variant"></div>
<div class="flex flex-col items-end">
<span class="text-label-xs text-on-surface-variant uppercase tracking-wider">현재 최저가</span>
<span class="text-title-sm font-bold text-primary">₩8,400,000</span>
</div>
</div>
</header>
<!-- Layout Grid -->
<div class="flex-1 flex overflow-hidden">
<!-- Left: Reference Viewer (2/3) -->
<section class="w-2/3 flex flex-col border-r border-outline-variant">
<!-- Tab Bar -->
<div class="flex bg-surface-container px-container-padding gap-8 h-12">
<button class="relative h-full flex items-center text-on-surface-variant font-label-md hover:text-primary transition-colors">
배치도
</button>
<button class="relative h-full flex items-center text-on-surface-variant font-label-md hover:text-primary transition-colors">
부스 설계안
</button>
<button class="relative h-full flex items-center text-on-surface-variant font-label-md hover:text-primary transition-colors">
물량서(BOQ)
</button>
<button class="relative h-full flex items-center text-primary font-bold font-label-md">
예상 이미지
<div class="active-tab-indicator"></div>
</button>
</div>
<!-- Image Display Area -->
<div class="flex-1 bg-black relative group overflow-hidden">
<div class="absolute inset-0 flex items-center justify-center">
<img class="w-full h-full object-cover" data-alt="A photorealistic architectural visualization of a high-tech KINTEX exhibition booth for Hanbit Robotics. The booth features sleek metallic curves, glowing integrated blue LED accent lighting, and multiple robotic arm demonstrations on pedestals. Large digital screens display technical data, and professional attendees are scattered around the spacious, modern layout. The lighting is crisp and cool, reflecting a futuristic, corporate innovation vibe." src="https://lh3.googleusercontent.com/aida/AP1WRLv3H3Wpe0zBHSuxQpTcXDcpOdolguqYeCc-O6Er1f1v7kIW1OvM8jO7ML3Duw169HILHRn6lMMYiX7cmJr_Ym05mrjgXagaQQtoAwQ3TIC3ZyZ7Bg-e42UBTkKt_wIydOFlskhmY6h5XTNmXwnDqY17JnXhXpY9NLdpPzY080hy03Mi5oNj1b3jZGV5WGcflIMxg6M49tHGG6P0pJY7mBYNrf9Lbn9jeoGeFbtHqCltSbrfa0LqRPTzk68"/>
</div>
<!-- Watermark -->
<div class="absolute bottom-6 right-8 text-white/40 text-label-xs italic pointer-events-none">
AI 생성 예상 이미지
</div>
<!-- Zoom/Fullscreen Controls Overlay -->
<div class="absolute top-6 right-6 flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<button class="w-10 h-10 glass-overlay rounded-lg flex items-center justify-center text-primary shadow-sm hover:scale-105 transition-transform">
<span class="material-symbols-outlined">zoom_in</span>
</button>
<button class="w-10 h-10 glass-overlay rounded-lg flex items-center justify-center text-primary shadow-sm hover:scale-105 transition-transform">
<span class="material-symbols-outlined">fullscreen</span>
</button>
</div>
</div>
</section>
<!-- Right: Live Ranking Panel (1/3) -->
<section class="w-1/3 flex flex-col bg-surface-container-low">
<!-- Title & Toggle -->
<div class="p-container-padding flex items-center justify-between">
<h2 class="text-title-sm font-bold text-on-surface flex items-center gap-2">
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">leaderboard</span>
실시간 순위
</h2>
<div class="flex items-center gap-2">
<span class="text-label-md text-on-surface-variant">익명 순위</span>
<button class="w-10 h-5 bg-primary-container rounded-full relative flex items-center px-1">
<div class="w-3.5 h-3.5 bg-white rounded-full translate-x-4 transition-transform"></div>
</button>
</div>
</div>
<!-- Ranking List -->
<div class="flex-1 px-container-padding overflow-y-auto custom-scrollbar">
<div class="space-y-4">
<!-- 1st Place -->
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant flex items-center gap-4 hover:shadow-md transition-all border-l-4 border-l-primary">
<div class="w-10 h-10 rounded-full bg-primary-fixed flex items-center justify-center shrink-0">
<span class="text-primary font-bold">1</span>
</div>
<div class="flex-1">
<p class="text-label-md font-bold text-on-surface">업체 A</p>
<p class="text-title-sm font-bold text-primary">₩8,400,000</p>
</div>
<div class="text-right">
<span class="text-label-xs px-2 py-0.5 bg-tertiary-fixed text-on-tertiary-fixed rounded-full">최저가</span>
</div>
</div>
<!-- 2nd Place (User Highlight) -->
<div class="bg-secondary-fixed/30 p-5 rounded-xl border-2 border-secondary-container flex items-center gap-4 hover:shadow-md transition-all scale-[1.02]">
<div class="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center shrink-0">
<span class="text-white font-bold">2</span>
</div>
<div class="flex-1">
<p class="text-label-md font-bold text-secondary">나의 응찰 (ME)</p>
<p class="text-title-sm font-bold text-secondary-container">₩8,700,000</p>
</div>
<div class="text-right flex flex-col items-end gap-1">
<span class="text-label-xs px-2 py-0.5 bg-secondary-container text-white rounded-full">나의 순위</span>
<p class="text-label-xs text-secondary-container font-bold">+ 300,000</p>
</div>
</div>
<!-- 3rd Place -->
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant flex items-center gap-4 hover:shadow-md transition-all">
<div class="w-10 h-10 rounded-full bg-surface-container-highest flex items-center justify-center shrink-0">
<span class="text-on-surface-variant font-bold">3</span>
</div>
<div class="flex-1">
<p class="text-label-md font-bold text-on-surface">업체 C</p>
<p class="text-title-sm font-bold text-on-surface">₩8,900,000</p>
</div>
</div>
<!-- Other placeholders for density -->
<div class="opacity-40 bg-surface-container-lowest p-4 rounded-xl border border-outline-variant border-dashed flex items-center gap-4">
<div class="w-8 h-8 rounded-full bg-surface-container flex items-center justify-center shrink-0">
<span class="text-on-surface-variant text-xs font-bold">4</span>
</div>
<div class="flex-1 h-4 bg-surface-container rounded-full"></div>
</div>
</div>
</div>
<!-- Footer Controls -->
<div class="p-container-padding bg-surface-container border-t border-outline-variant space-y-4">
<div class="flex flex-col items-center bg-white/50 rounded-xl py-4 border border-outline-variant/50">
<span class="text-label-xs text-on-surface-variant font-bold uppercase tracking-[0.2em] mb-1">라운드 종료까지</span>
<div class="flex items-baseline gap-2">
<span class="text-[32px] font-bold text-primary tracking-tighter" id="countdown">01:24:36</span>
</div>
</div>
<button class="w-full py-4 bg-primary text-white rounded-xl font-bold text-title-sm flex items-center justify-center gap-3 hover:bg-primary-container shadow-xl active:scale-[0.98] transition-all">
<span class="material-symbols-outlined">edit_note</span>
견적서 작성 · 재응찰
</button>
<p class="text-[11px] text-on-surface-variant text-center px-4">
* 재응찰 시 이전 응찰 정보는 자동으로 대체됩니다. 입찰 보증금은 KINTEX AI 에스크로 시스템에 보관됩니다.
</p>
</div>
</section>
</div>
</main>
</div>
<script>
// Simple countdown logic for visual effect
let timeLeft = 5076; // 01:24:36 in seconds
const countdownEl = document.getElementById('countdown');
function updateCountdown() {
const h = Math.floor(timeLeft / 3600);
const m = Math.floor((timeLeft % 3600) / 60);
const s = timeLeft % 60;
countdownEl.textContent =
`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
if (timeLeft > 0) timeLeft--;
}
setInterval(updateCountdown, 1000);
</script>
</body></html>

View File

@ -1,461 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>SCR-29 Award Comparison and Selection - KINTEX AI System</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&amp;family=Geist:wght@500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-secondary-fixed-variant": "#4500d8",
"inverse-primary": "#a2c9ff",
"outline-variant": "#c1c7d3",
"secondary-fixed-dim": "#c9bfff",
"on-surface": "#141b2c",
"on-primary-container": "#91c0ff",
"tertiary": "#003e28",
"error-red": "#ba1a1a",
"primary": "#003765",
"on-error-container": "#93000a",
"inverse-on-surface": "#eef0ff",
"on-error": "#ffffff",
"secondary-fixed": "#e5deff",
"surface-dim": "#d3d9f1",
"on-tertiary-fixed-variant": "#005236",
"surface-container": "#eaedff",
"surface-container-lowest": "#ffffff",
"primary-fixed": "#d3e4ff",
"on-tertiary-container": "#68d1a0",
"tertiary-container": "#00583b",
"on-primary": "#ffffff",
"error": "#ba1a1a",
"surface-container-low": "#f2f3ff",
"tertiary-fixed-dim": "#72daa9",
"on-secondary": "#ffffff",
"on-secondary-fixed": "#1b0063",
"tertiary-fixed": "#8ef7c4",
"on-tertiary": "#ffffff",
"surface-tint": "#24609e",
"surface-bright": "#faf8ff",
"on-primary-fixed-variant": "#004881",
"surface-container-high": "#e1e7ff",
"surface-variant": "#dbe2fa",
"on-primary-fixed": "#001c38",
"primary-container": "#004e8b",
"outline": "#727781",
"surface": "#faf8ff",
"error-container": "#ffdad6",
"secondary": "#5427e6",
"on-background": "#141b2c",
"on-tertiary-fixed": "#002113",
"on-surface-variant": "#424750",
"inverse-surface": "#293042",
"on-secondary-container": "#f3edff",
"background": "#faf8ff",
"surface-container-highest": "#dbe2fa",
"secondary-container": "#6d4aff",
"primary-fixed-dim": "#a2c9ff",
"success-accent": "#0E8A5F",
"ai-accent": "#6D4AFF"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "24px",
"container-padding": "24px",
"base": "4px",
"stack-sm": "8px",
"gutter": "16px",
"stack-md": "16px",
"section-padding": "96px"
},
"fontFamily": {
"display-lg": ["Inter"],
"body-sm": ["Inter"],
"body-base": ["Inter"],
"label-md": ["Geist"],
"headline-md": ["Inter"],
"title-sm": ["Inter"]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.bento-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 16px;
}
</style>
</head>
<body class="bg-surface-bright text-on-surface font-body-base">
<!-- Sidebar Navigation -->
<aside class="fixed left-0 top-0 h-full w-64 bg-surface-container dark:bg-surface-container-highest border-r border-outline-variant flex flex-col py-stack-lg z-50">
<div class="px-6 mb-8">
<div class="flex items-center gap-3 mb-2">
<img alt="KINTEX Logo" class="h-8" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD2uFbO3hsi6PdyRzy9wGZT5p3cB4GLFaPvgwa2hSZwLGtZnGItAfHsy-sLZAsVLRR8GUsODMkbR9FfxkNArjLgmJuS8uuCA9CTFbm6kOg-O-Jj0UXGORfPVCjHME1lp4KqBmB6o74N6btUDC6H9XXip92mK7EpkmvdH7QS-yJ_LL7W-xACAEYeAuOXeKaG6LuhUUsv2xdUZTZjjFD8dDz19wbQraGkMuDaUpKh8ot7eEJrQx70l5kdlBeLyc80bRgz3vBo1xdRi5s"/>
<span class="font-headline-md text-headline-md font-bold text-primary">KINTEX</span>
</div>
<p class="font-label-md text-label-md text-on-surface-variant">Management Portal</p>
</div>
<nav class="flex-1 px-3 space-y-1">
<div class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer hover:bg-surface-container-high transition-all text-on-surface-variant">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">Dashboard</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer hover:bg-surface-container-high transition-all text-on-surface-variant">
<span class="material-symbols-outlined">calendar_today</span>
<span class="font-label-md text-label-md">Schedule</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 rounded-xl bg-primary-fixed text-primary font-bold border-l-4 border-primary">
<span class="material-symbols-outlined">gavel</span>
<span class="font-label-md text-label-md">Auction</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer hover:bg-surface-container-high transition-all text-on-surface-variant">
<span class="material-symbols-outlined">domain</span>
<span class="font-label-md text-label-md">Hall Operations</span>
</div>
</nav>
<div class="mt-auto px-3 border-t border-outline-variant pt-6">
<div class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer hover:bg-surface-container-high transition-all text-on-surface-variant">
<span class="material-symbols-outlined">contact_support</span>
<span class="font-label-md text-label-md">Support</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer hover:bg-surface-container-high transition-all text-on-surface-variant">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-md text-label-md">Settings</span>
</div>
</div>
</aside>
<!-- Top Navigation Bar -->
<header class="fixed top-0 left-64 right-0 h-16 bg-surface-bright border-b border-outline-variant flex justify-between items-center px-container-padding z-40">
<div class="flex items-center gap-6">
<div class="flex items-center gap-2 bg-surface-container-low px-4 py-1.5 rounded-full border border-outline-variant">
<span class="material-symbols-outlined text-outline">search</span>
<input class="bg-transparent border-none focus:ring-0 text-body-base w-64" placeholder="입찰 번호 또는 전시회 검색" type="text"/>
</div>
<nav class="hidden md:flex gap-6">
<a class="text-primary font-bold border-b-2 border-primary py-4" href="#">Global View</a>
<a class="text-on-surface-variant hover:text-primary transition-colors py-4" href="#">Analytics</a>
<a class="text-on-surface-variant hover:text-primary transition-colors py-4" href="#">Reports</a>
<a class="text-on-surface-variant hover:text-primary transition-colors py-4" href="#">Settings</a>
</nav>
</div>
<div class="flex items-center gap-4">
<button class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all">
<span class="material-symbols-outlined text-on-surface-variant">notifications</span>
</button>
<button class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all">
<span class="material-symbols-outlined text-on-surface-variant">help</span>
</button>
<div class="flex items-center gap-2 cursor-pointer active:scale-95 transition-all">
<div class="w-8 h-8 rounded-full bg-primary-container flex items-center justify-center">
<span class="material-symbols-outlined text-on-primary-container text-[20px]">account_circle</span>
</div>
<span class="font-label-md text-label-md">Exhibition Manager</span>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="ml-64 pt-16 flex min-h-screen">
<div class="flex-1 p-stack-lg max-w-[calc(100%-360px)]">
<!-- Page Header -->
<div class="mb-8 flex justify-between items-end">
<div>
<nav class="flex items-center gap-2 text-label-xs text-on-surface-variant mb-2">
<span>Auction Management</span>
<span class="material-symbols-outlined text-[12px]">chevron_right</span>
<span>Quotation Comparison</span>
</nav>
<h1 class="font-display-lg text-display-lg text-primary mb-2">SCR-29 역경매 응찰 비교 및 낙찰</h1>
<div class="flex items-center gap-4 mt-4">
<div class="px-4 py-2 bg-surface-container rounded-lg border-l-4 border-primary">
<p class="text-label-xs text-on-surface-variant">공고명</p>
<p class="font-title-sm text-title-sm">A-102 독립부스 시공</p>
</div>
<div class="px-4 py-2 bg-surface-container rounded-lg border-l-4 border-secondary">
<p class="text-label-xs text-on-surface-variant">응찰 현황</p>
<p class="font-title-sm text-title-sm">총 5개사 참여</p>
</div>
<div class="px-4 py-2 bg-surface-container rounded-lg border-l-4 border-tertiary">
<p class="text-label-xs text-on-surface-variant">낙찰기준</p>
<p class="font-title-sm text-title-sm">종합평가 (가격 40% + 역량 60%)</p>
</div>
</div>
</div>
<button class="bg-surface-container-low border border-outline-variant px-4 py-2 rounded-xl flex items-center gap-2 font-label-md hover:bg-surface-container-high transition-all">
<span class="material-symbols-outlined">download</span>
응찰서 원본 다운로드
</button>
</div>
<!-- Comparison Matrix -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-sm">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-surface-container-low border-b border-outline-variant">
<th class="p-6 font-label-md text-on-surface-variant w-1/4">비교 항목</th>
<th class="p-6 font-headline-md text-headline-md text-primary text-center relative">
<div class="absolute top-0 left-1/2 -translate-x-1/2 px-3 py-1 bg-primary text-white text-[10px] font-bold rounded-b-lg">최적 추천</div>
업체 A
<span class="block text-label-xs font-normal text-on-surface-variant mt-1">종합 1위 (추천)</span>
</th>
<th class="p-6 font-headline-md text-headline-md text-on-surface text-center">
업체 B
<span class="block text-label-xs font-normal text-on-surface-variant mt-1">종합 2위</span>
</th>
<th class="p-6 font-headline-md text-headline-md text-on-surface text-center">
업체 C
<span class="block text-label-xs font-normal text-on-surface-variant mt-1">종합 3위</span>
</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-outline-variant hover:bg-surface-container-low transition-colors">
<td class="p-6 font-title-sm text-title-sm">총액 (VAT 별도)</td>
<td class="p-6 text-center text-success-accent font-bold bg-success-accent/5">
<span class="text-headline-md">₩8,400,000</span>
<div class="mt-1 flex items-center justify-center gap-1 text-label-xs">
<span class="material-symbols-outlined text-[14px]">arrow_downward</span> 최저가
</div>
</td>
<td class="p-6 text-center">
<span class="text-title-sm">₩8,700,000</span>
<div class="mt-1 text-label-xs text-on-surface-variant">+3.5% 차이</div>
</td>
<td class="p-6 text-center">
<span class="text-title-sm">₩8,900,000</span>
<div class="mt-1 text-label-xs text-on-surface-variant">+5.9% 차이</div>
</td>
</tr>
<tr class="border-b border-outline-variant hover:bg-surface-container-low transition-colors">
<td class="p-6 font-title-sm text-title-sm">납기 (시공 완료)</td>
<td class="p-6 text-center text-on-surface">12일</td>
<td class="p-6 text-center text-success-accent font-bold bg-success-accent/5">
<span class="text-title-sm">10일</span>
<div class="mt-1 flex items-center justify-center gap-1 text-label-xs">
최단 납기
</div>
</td>
<td class="p-6 text-center text-on-surface">14일</td>
</tr>
<tr class="border-b border-outline-variant hover:bg-surface-container-low transition-colors">
<td class="p-6 font-title-sm text-title-sm">업체 평판 (최근 1년)</td>
<td class="p-6 text-center text-success-accent font-bold bg-success-accent/5">
<div class="flex items-center justify-center gap-1 text-title-sm">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">star</span>
4.8
</div>
</td>
<td class="p-6 text-center">
<div class="flex items-center justify-center gap-1 text-on-surface">
<span class="material-symbols-outlined">star</span>
4.6
</div>
</td>
<td class="p-6 text-center">
<div class="flex items-center justify-center gap-1 text-on-surface">
<span class="material-symbols-outlined">star</span>
4.2
</div>
</td>
</tr>
<tr class="hover:bg-surface-container-low transition-colors">
<td class="p-6 font-title-sm text-title-sm">종합 점수</td>
<td class="p-6 text-center bg-ai-accent/10 border-2 border-ai-accent/30 rounded-xl m-2">
<div class="flex flex-col items-center gap-2">
<span class="text-display-lg text-ai-accent">92.4</span>
<span class="px-2 py-0.5 bg-ai-accent text-white rounded text-[10px] font-bold">AI 가중 스코어</span>
</div>
</td>
<td class="p-6 text-center">
<span class="text-headline-md text-on-surface-variant">89.1</span>
</td>
<td class="p-6 text-center">
<span class="text-headline-md text-on-surface-variant">84.7</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Visualization Section -->
<div class="mt-8 bento-grid">
<div class="col-span-8 bg-surface-container-lowest border border-outline-variant rounded-xl p-6">
<h3 class="font-title-sm text-title-sm mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-ai-accent">analytics</span>
AI 시뮬레이션: 리스크 분석
</h3>
<div class="flex gap-8 items-center h-48">
<div class="flex-1 space-y-4">
<div>
<div class="flex justify-between text-label-xs mb-1">
<span>시공 지연 확률</span>
<span class="text-success-accent font-bold">2.4% (낮음)</span>
</div>
<div class="h-2 bg-surface-container rounded-full overflow-hidden">
<div class="h-full bg-success-accent w-[2.4%]"></div>
</div>
</div>
<div>
<div class="flex justify-between text-label-xs mb-1">
<span>예산 초과 위험</span>
<span class="text-on-surface-variant">12.8% (보통)</span>
</div>
<div class="h-2 bg-surface-container rounded-full overflow-hidden">
<div class="h-full bg-ai-accent w-[12.8%]"></div>
</div>
</div>
<div>
<div class="flex justify-between text-label-xs mb-1">
<span>자재 품질 신뢰도</span>
<span class="text-success-accent font-bold">96.5% (매우 높음)</span>
</div>
<div class="h-2 bg-surface-container rounded-full overflow-hidden">
<div class="h-full bg-success-accent w-[96.5%]"></div>
</div>
</div>
</div>
<div class="w-1/3 h-full flex flex-col justify-center items-center bg-surface-container-low rounded-xl border border-outline-variant">
<span class="text-label-xs text-on-surface-variant mb-1">AI 최종 등급</span>
<span class="text-[48px] font-bold text-ai-accent">A+</span>
<span class="text-label-xs text-success-accent">안정적인 시공 보장</span>
</div>
</div>
</div>
<div class="col-span-4 bg-surface-container-lowest border border-outline-variant rounded-xl p-6 relative overflow-hidden">
<div class="absolute top-0 right-0 p-4 opacity-10">
<span class="material-symbols-outlined text-[64px]">verified</span>
</div>
<h3 class="font-title-sm text-title-sm mb-4">히스토리 분석</h3>
<p class="text-body-base text-on-surface-variant mb-4 leading-relaxed">
업체 A는 지난 3년간 KINTEX 내 유사 규모(100-150sqm) 시공을 총 12회 수행하였으며, 모든 건에 대해 클레임 발생률 0%를 기록하고 있습니다.
</p>
<div class="flex flex-wrap gap-2">
<span class="px-2 py-1 bg-surface-container rounded text-label-xs">#최근수행_완벽</span>
<span class="px-2 py-1 bg-surface-container rounded text-label-xs">#안전_1등급</span>
<span class="px-2 py-1 bg-surface-container rounded text-label-xs">#가격_합리적</span>
</div>
</div>
</div>
</div>
<!-- Right Side Action Panel -->
<aside class="w-[360px] bg-surface-container-low border-l border-outline-variant p-stack-lg sticky top-16 h-[calc(100vh-64px)] overflow-y-auto">
<h2 class="font-headline-md text-headline-md text-primary mb-6">낙찰 선정</h2>
<div class="space-y-6">
<!-- Highlight Card -->
<div class="bg-surface-container-lowest border border-primary p-6 rounded-xl shadow-md relative">
<div class="absolute top-4 right-4 bg-success-accent/10 text-success-accent px-2 py-0.5 rounded text-[10px] font-bold">1st Choice</div>
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-white font-bold text-xl">A</div>
<div>
<p class="font-title-sm text-title-sm">업체 A (종합 1위)</p>
<div class="flex items-center gap-1 text-ai-accent text-label-md">
<span class="material-symbols-outlined text-[16px]">smart_toy</span>
신뢰도 98% 분석
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-label-xs pt-4 border-t border-outline-variant">
<div>
<span class="block text-on-surface-variant">제안 가격</span>
<span class="font-bold text-on-surface">₩8,400,000</span>
</div>
<div>
<span class="block text-on-surface-variant">예상 기한</span>
<span class="font-bold text-on-surface">12일</span>
</div>
</div>
</div>
<!-- Input Section -->
<div class="space-y-4">
<label class="block font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">선정 사유 입력 (필수)</label>
<textarea class="w-full h-32 bg-surface-container-lowest border border-outline-variant rounded-xl p-4 text-body-base focus:ring-primary focus:border-primary placeholder:text-outline-variant" placeholder="종합 평가 결과 및 업체의 강점을 입력해 주세요. 예: 최저가 응찰 및 과거 동일 규모 행사 수행 실적 우수..."></textarea>
</div>
<!-- Action Button -->
<div class="space-y-3 pt-4">
<button class="w-full bg-success-accent text-white h-14 rounded-xl font-bold flex items-center justify-center gap-2 hover:brightness-110 active:scale-[0.98] transition-all shadow-lg shadow-success-accent/20">
<span class="material-symbols-outlined">how_to_reg</span>
낙찰(Award) 최종 승인
</button>
<button class="w-full bg-surface-container-highest text-on-surface-variant h-12 rounded-xl font-bold hover:bg-surface-dim transition-all">
재입찰 요청
</button>
<div class="flex items-start gap-2 p-4 bg-error-container/30 rounded-lg text-error text-[11px] leading-relaxed">
<span class="material-symbols-outlined text-[16px] mt-0.5">info</span>
<p>주의: 낙찰 선정 후에는 취소가 불가하며, 해당 견적서는 즉시 계약·발주 공문서로 전환되어 법적 효력을 갖게 됩니다.</p>
</div>
</div>
<!-- Helper Footer -->
<div class="pt-8 border-t border-outline-variant">
<p class="text-label-xs text-on-surface-variant font-bold mb-3">낙찰 이후 프로세스</p>
<ul class="space-y-3">
<li class="flex gap-3 text-label-xs text-on-surface-variant">
<span class="w-5 h-5 rounded-full bg-surface-container flex items-center justify-center text-[10px]">1</span>
전자 서명 및 계약 체결
</li>
<li class="flex gap-3 text-label-xs text-on-surface-variant">
<span class="w-5 h-5 rounded-full bg-surface-container flex items-center justify-center text-[10px]">2</span>
시공 착수 회의 예약
</li>
<li class="flex gap-3 text-label-xs text-on-surface-variant">
<span class="w-5 h-5 rounded-full bg-surface-container flex items-center justify-center text-[10px]">3</span>
정기 공정 보고서 발송
</li>
</ul>
</div>
</div>
</aside>
</main>
<!-- Floating AI Assistant Chat (Micro-interaction) -->
<div class="fixed bottom-8 right-8 z-[60]">
<button class="w-14 h-14 bg-ai-accent text-white rounded-full flex items-center justify-center shadow-xl hover:scale-110 active:scale-95 transition-all group overflow-hidden">
<!-- Placeholder for future expansion -->
<span class="material-symbols-outlined text-3xl transition-transform group-hover:rotate-12">psychology</span>
</button>
<div class="absolute bottom-full right-0 mb-4 w-64 glass-panel border border-outline-variant p-4 rounded-2xl shadow-2xl opacity-0 translate-y-4 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-300">
<p class="text-label-xs text-ai-accent font-bold mb-1">AI Recommendation Insight</p>
<p class="text-body-sm leading-snug">
"업체 A는 작년 '2023 국제모터쇼'에서 동일한 재질의 부스를 제안가보다 5% 낮은 최종 정산가로 완공한 이력이 있어 가장 추천합니다."
</p>
</div>
</div>
<script>
// Simple micro-interaction for the recommendation highlight
document.addEventListener('DOMContentLoaded', () => {
const aiButton = document.querySelector('.fixed.bottom-8.right-8');
const tooltip = aiButton.querySelector('.glass-panel');
aiButton.addEventListener('mouseenter', () => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(0)';
tooltip.style.pointerEvents = 'auto';
});
aiButton.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(1rem)';
tooltip.style.pointerEvents = 'none';
});
});
</script>
</body></html>

View File

@ -1,544 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
.glass-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(193, 199, 211, 0.5);
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-tertiary": "#ffffff",
"on-secondary": "#ffffff",
"on-tertiary-fixed": "#002113",
"outline-variant": "#c1c7d3",
"surface": "#faf8ff",
"tertiary-container": "#00583b",
"surface-bright": "#faf8ff",
"on-primary": "#ffffff",
"primary-fixed": "#d3e4ff",
"tertiary-fixed-dim": "#72daa9",
"tertiary": "#003e28",
"on-error-container": "#93000a",
"inverse-on-surface": "#eef0ff",
"surface-container-highest": "#dbe2fa",
"primary-fixed-dim": "#a2c9ff",
"on-tertiary-fixed-variant": "#005236",
"inverse-primary": "#a2c9ff",
"secondary-fixed": "#e5deff",
"secondary-container": "#6d4aff",
"error-red": "#ba1a1a",
"on-tertiary-container": "#68d1a0",
"surface-container-lowest": "#ffffff",
"error-container": "#ffdad6",
"on-primary-fixed": "#001c38",
"on-background": "#141b2c",
"on-secondary-container": "#f3edff",
"tertiary-fixed": "#8ef7c4",
"inverse-surface": "#293042",
"surface-tint": "#24609e",
"surface-variant": "#dbe2fa",
"on-error": "#ffffff",
"on-secondary-fixed-variant": "#4500d8",
"secondary": "#5427e6",
"primary": "#003765",
"on-secondary-fixed": "#1b0063",
"surface-container": "#eaedff",
"primary-container": "#004e8b",
"surface-container-high": "#e1e7ff",
"surface-container-low": "#f2f3ff",
"on-primary-container": "#91c0ff",
"surface-dim": "#d3d9f1",
"background": "#faf8ff",
"secondary-fixed-dim": "#c9bfff",
"on-surface": "#141b2c",
"error": "#ba1a1a",
"outline": "#727781",
"on-primary-fixed-variant": "#004881",
"on-surface-variant": "#424750"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"container-padding": "24px",
"stack-sm": "8px",
"stack-lg": "24px",
"section-padding": "96px",
"gutter": "16px",
"stack-md": "16px",
"base": "4px"
},
"fontFamily": {
"label-xs": ["Geist"],
"headline-md": ["Inter"],
"display-lg": ["Inter"],
"label-md": ["Geist"],
"title-sm": ["Inter"],
"body-sm": ["Inter"],
"body-base": ["Inter"]
}
}
}
}
</script>
</head>
<body class="bg-surface-bright text-on-surface font-body-base antialiased">
<!-- Sidebar Navigation -->
<aside class="w-[240px] h-screen fixed left-0 top-0 bg-inverse-surface flex flex-col py-stack-lg px-gutter border-r border-outline-variant z-50">
<div class="mb-stack-lg">
<img alt="KINTEX Admin Logo" class="h-10 object-contain brightness-0 invert" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD-YApYFpKErWYFPUlF6AJaJ1HO1UjoYAGxp255OT9ViuUZdP3MO_wVCjJL7vnauUVxTZxUjVtAU9lnu_gwEXzs7WTduIDg1_MrAL8R5xOY_Co7ZSWTIZa0jXsoGg2yImMaGxT6EUHJFqJMbgtykwzipJRVZKhRC0EgxIMBX3NZHTv8vgdK6npSj6FaTJ8wRt6FjF3K3w2KXT2R4C_NL8PvRpcmx2FBzJ5-2jMHT0XUuPbvsadhqSDqyG1F8iLFCbs6JhjPPRUyj5w"/>
<div class="mt-4 px-2">
<h1 class="font-display-lg text-[20px] text-on-primary-fixed tracking-tight">KINTEX Visitor Admin</h1>
<p class="font-label-md text-surface-variant text-[11px]">글로벌 마이스 플랫폼</p>
</div>
</div>
<nav class="flex-1 space-y-1 overflow-y-auto no-scrollbar">
<div class="px-2 py-1 flex items-center gap-3 rounded-lg cursor-pointer transition-colors text-surface-variant hover:bg-surface-container-highest/10">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">대시보드</span>
</div>
<!-- Active Item: 관람객 -->
<div class="px-2 py-2.5 flex items-center gap-3 rounded-lg cursor-pointer bg-secondary-container text-on-secondary-container font-bold border-l-4 border-secondary transition-all">
<span class="material-symbols-outlined">group</span>
<span class="font-label-md text-label-md">관람객</span>
</div>
<div class="px-2 py-1 flex items-center gap-3 rounded-lg cursor-pointer transition-colors text-surface-variant hover:bg-surface-container-highest/10">
<span class="material-symbols-outlined">event_available</span>
<span class="font-label-md text-label-md">전시 관리</span>
</div>
<div class="px-2 py-1 flex items-center gap-3 rounded-lg cursor-pointer transition-colors text-surface-variant hover:bg-surface-container-highest/10">
<span class="material-symbols-outlined">assignment</span>
<span class="font-label-md text-label-md">신청 폼</span>
</div>
<div class="px-2 py-1 flex items-center gap-3 rounded-lg cursor-pointer transition-colors text-surface-variant hover:bg-surface-container-highest/10">
<span class="material-symbols-outlined">analytics</span>
<span class="font-label-md text-label-md">통계 분석</span>
</div>
<div class="px-2 py-1 flex items-center gap-3 rounded-lg cursor-pointer transition-colors text-surface-variant hover:bg-surface-container-highest/10">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-md text-label-md">시스템 설정</span>
</div>
</nav>
<div class="mt-auto pt-stack-lg border-t border-outline/20 space-y-1">
<button class="w-full flex items-center gap-2 px-4 py-3 bg-primary-container text-on-primary-container rounded-full text-label-md font-bold mb-4 transition-transform active:scale-95">
<span class="material-symbols-outlined text-[18px]">auto_awesome</span>
AI 분석 리포트
</button>
<div class="px-2 py-1 flex items-center gap-3 text-surface-variant cursor-pointer hover:text-on-surface-variant">
<span class="material-symbols-outlined">help</span>
<span class="font-label-md text-label-md">헬프센터</span>
</div>
<div class="px-2 py-1 flex items-center gap-3 text-surface-variant cursor-pointer hover:text-on-surface-variant">
<span class="material-symbols-outlined">logout</span>
<span class="font-label-md text-label-md">로그아웃</span>
</div>
</div>
</aside>
<!-- Top Navigation Bar -->
<header class="fixed top-0 left-[240px] w-[calc(100%-240px)] h-16 bg-surface-bright flex justify-between items-center px-container-padding border-b border-outline-variant z-40">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary cursor-pointer">menu_open</span>
<h2 class="font-headline-md text-headline-md text-primary">관람객 데이터 관리</h2>
</div>
<div class="flex items-center gap-6">
<div class="relative group">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline">search</span>
<input class="pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm w-64 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="검색어를 입력하세요..." type="text"/>
</div>
<div class="flex items-center gap-4 text-on-surface-variant">
<span class="material-symbols-outlined cursor-pointer hover:text-primary transition-colors">notifications</span>
<span class="material-symbols-outlined cursor-pointer hover:text-primary transition-colors">security</span>
<div class="flex items-center gap-2 cursor-pointer border-l border-outline-variant pl-4">
<span class="material-symbols-outlined text-[32px]">account_circle</span>
<div class="text-left hidden lg:block">
<p class="text-label-md font-bold leading-tight">Admin User</p>
<p class="text-[10px] text-outline">KINTEX System</p>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="ml-[240px] mt-16 p-container-padding space-y-6">
<!-- KPI Row -->
<section class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-gutter">
<!-- KPI 1 -->
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-primary shadow-sm">
<div class="flex justify-between items-start mb-2">
<p class="text-on-surface-variant font-label-md">사전등록</p>
<span class="material-symbols-outlined text-primary text-[20px]">how_to_reg</span>
</div>
<div class="flex items-baseline gap-2">
<h3 class="text-[28px] font-bold">12,480</h3>
<span class="bg-primary-fixed text-on-primary-fixed text-[10px] px-1.5 py-0.5 rounded font-bold">+12.4%</span>
</div>
</div>
<!-- KPI 2 -->
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-secondary shadow-sm">
<div class="flex justify-between items-start mb-2">
<p class="text-on-surface-variant font-label-md">체크인</p>
<span class="material-symbols-outlined text-secondary text-[20px]">check_circle</span>
</div>
<h3 class="text-[28px] font-bold">8,210</h3>
<div class="w-full bg-surface-container h-1.5 rounded-full mt-2 overflow-hidden">
<div class="bg-secondary h-full" style="width: 65%;"></div>
</div>
<p class="text-[10px] text-outline mt-1 text-right">Progress: 65.7%</p>
</div>
<!-- KPI 3 -->
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-error shadow-sm">
<div class="flex justify-between items-start mb-2">
<p class="text-on-surface-variant font-label-md">노쇼율</p>
<span class="material-symbols-outlined text-error text-[20px]">event_busy</span>
</div>
<div class="flex items-center gap-2">
<h3 class="text-[28px] font-bold">12%</h3>
<span class="flex items-center gap-0.5 text-error font-bold text-[11px]">
<span class="material-symbols-outlined text-[14px]">warning</span> Alert
</span>
</div>
</div>
<!-- KPI 4 -->
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-tertiary shadow-sm">
<div class="flex justify-between items-start mb-2">
<p class="text-on-surface-variant font-label-md">바이어 비중</p>
<span class="material-symbols-outlined text-tertiary text-[20px]">business_center</span>
</div>
<h3 class="text-[28px] font-bold">34%</h3>
<p class="text-[10px] text-outline">Target: 30%</p>
</div>
<!-- KPI 5 -->
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-secondary-container shadow-sm overflow-hidden relative">
<div class="flex justify-between items-start mb-2 relative z-10">
<p class="text-on-surface-variant font-label-md">리드 생성</p>
<span class="material-symbols-outlined text-secondary text-[20px]">auto_awesome</span>
</div>
<h3 class="text-[28px] font-bold text-secondary relative z-10">2,140</h3>
<p class="text-[10px] text-outline relative z-10">AI Predicted Performance</p>
<!-- Subtle background glow -->
<div class="absolute -right-4 -bottom-4 w-16 h-16 bg-secondary/10 blur-2xl rounded-full"></div>
</div>
</section>
<!-- Main Content Area -->
<section class="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
<!-- Left 2/3: Trend Chart -->
<div class="lg:col-span-2 bg-surface-container-lowest rounded-xl border border-outline-variant p-stack-lg shadow-sm">
<div class="flex justify-between items-center mb-6">
<h4 class="font-title-sm text-title-sm">등록 및 체크인 추이</h4>
<div class="flex gap-2">
<div class="flex items-center gap-1">
<span class="w-3 h-3 bg-[#0066B3] rounded-full"></span>
<span class="text-label-xs">사전등록</span>
</div>
<div class="flex items-center gap-1">
<span class="w-3 h-3 bg-outline rounded-full"></span>
<span class="text-label-xs">체크인</span>
</div>
</div>
</div>
<div class="h-64 relative">
<!-- Chart Placeholder via CSS Gradients & Lines -->
<div class="absolute inset-0 flex items-end justify-between px-2 pt-4">
<div class="absolute inset-0 grid grid-rows-4 w-full h-full">
<div class="border-b border-surface-container-high w-full h-full"></div>
<div class="border-b border-surface-container-high w-full h-full"></div>
<div class="border-b border-surface-container-high w-full h-full"></div>
<div class="border-b border-surface-container-high w-full h-full"></div>
</div>
<!-- Abstract Area Chart UI -->
<svg class="w-full h-full relative z-10" preserveaspectratio="none" viewbox="0 0 100 100">
<!-- Blue Area -->
<path d="M0,100 L0,80 C10,75 20,40 30,50 C40,60 50,20 60,35 C70,50 80,10 90,5 C100,0 100,100 L100,100 Z" fill="rgba(0,102,179,0.1)" stroke="#0066B3" stroke-width="2"></path>
<!-- Gray Area -->
<path d="M0,100 L0,90 C10,88 20,70 30,75 C40,80 50,50 60,65 C70,75 80,40 90,35 C100,30 100,100 L100,100 Z" fill="rgba(114,119,129,0.1)" stroke="#727781" stroke-width="2"></path>
</svg>
</div>
<!-- Inset Donut Chart -->
<div class="absolute bottom-4 right-4 w-32 h-32 glass-card rounded-xl p-3 flex flex-col items-center justify-center border border-outline-variant">
<p class="text-[10px] font-bold mb-2">등록 유형</p>
<div class="relative w-16 h-16">
<svg class="w-full h-full" viewbox="0 0 36 36">
<circle cx="18" cy="18" fill="transparent" r="15.9" stroke="#e1e7ff" stroke-width="4"></circle>
<circle cx="18" cy="18" fill="transparent" r="15.9" stroke="#003765" stroke-dasharray="62, 100" stroke-dashoffset="25" stroke-width="4"></circle>
<circle cx="18" cy="18" fill="transparent" r="15.9" stroke="#5427e6" stroke-dasharray="34, 100" stroke-dashoffset="-37" stroke-width="4"></circle>
</svg>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-[10px] font-bold">100%</span>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="text-[8px]"><span class="w-1 h-1 bg-primary inline-block mr-0.5"></span>62%</div>
<div class="text-[8px]"><span class="w-1 h-1 bg-secondary inline-block mr-0.5"></span>34%</div>
</div>
</div>
</div>
<div class="flex justify-between mt-4 text-[11px] text-outline px-2">
<span>D-14</span><span>D-10</span><span>D-7</span><span>D-3</span><span>Today</span>
</div>
</div>
<!-- Right 1/3: Forms & Badge -->
<div class="space-y-gutter">
<!-- Registration Forms Management -->
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant p-stack-md shadow-sm">
<div class="flex justify-between items-center mb-4">
<h4 class="font-title-sm text-title-sm">등록 폼 관리</h4>
<span class="material-symbols-outlined text-outline cursor-pointer">more_horiz</span>
</div>
<div class="space-y-3 mb-4">
<div class="flex items-center justify-between p-2 hover:bg-surface-container-low rounded-lg transition-colors">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-primary rounded-full"></div>
<span class="text-body-sm font-medium">일반 참관객</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input checked="" class="sr-only peer" type="checkbox"/>
<div class="w-9 h-5 bg-outline-variant peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
<div class="flex items-center justify-between p-2 hover:bg-surface-container-low rounded-lg transition-colors">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-secondary rounded-full"></div>
<span class="text-body-sm font-medium">비즈니스 바이어</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input checked="" class="sr-only peer" type="checkbox"/>
<div class="w-9 h-5 bg-outline-variant peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
<div class="flex items-center justify-between p-2 hover:bg-surface-container-low rounded-lg transition-colors">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-tertiary-container rounded-full"></div>
<span class="text-body-sm font-medium">VIP 초청</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input class="sr-only peer" type="checkbox"/>
<div class="w-9 h-5 bg-outline-variant peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
</div>
<button class="w-full bg-secondary text-on-secondary py-3 rounded-xl flex items-center justify-center gap-2 font-bold transition-all hover:brightness-110 active:scale-95 shadow-md">
<span class="material-symbols-outlined">auto_awesome</span>
AI 폼빌더 시작하기
</button>
</div>
<!-- Badge Template Preview -->
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant p-stack-md shadow-sm">
<h4 class="font-title-sm text-title-sm mb-4">배지 템플릿</h4>
<div class="relative bg-surface-container-high rounded-lg p-4 flex flex-col items-center">
<div class="bg-white w-32 h-44 rounded-md shadow-lg border border-outline-variant overflow-hidden flex flex-col">
<div class="h-6 bg-primary flex items-center justify-center px-1">
<span class="text-[6px] text-white font-bold tracking-widest">KINTEX VISITOR</span>
</div>
<div class="p-2 flex-1 flex flex-col items-center">
<div class="w-12 h-12 bg-surface-container mt-2 flex items-center justify-center">
<span class="material-symbols-outlined text-outline text-[40px]">qr_code_2</span>
</div>
<div class="mt-4 text-center">
<p class="text-[10px] font-bold text-primary">KIM **</p>
<p class="text-[6px] text-outline mt-1 uppercase">Samsung Electronics</p>
</div>
<div class="mt-auto w-full flex justify-between items-end">
<div class="h-4 w-8 bg-secondary-container rounded-sm flex items-center justify-center">
<span class="text-[4px] text-on-secondary-container font-black">BUYER</span>
</div>
<div class="text-[5px] text-outline-variant font-mono">2024-V01</div>
</div>
</div>
</div>
<button class="mt-4 w-full bg-surface-container-lowest border border-outline-variant py-2 rounded-lg text-label-md hover:bg-surface-container transition-colors">편집하기</button>
</div>
</div>
</div>
</section>
<!-- Bottom Section: Data Table -->
<section class="bg-surface-container-lowest rounded-xl border border-outline-variant shadow-sm overflow-hidden">
<div class="px-stack-lg py-stack-md border-b border-outline-variant flex justify-between items-center bg-surface-container-low">
<h4 class="font-title-sm text-title-sm">전체 신청자 명단</h4>
<div class="flex gap-2">
<button class="px-3 py-1.5 bg-white border border-outline-variant rounded-md text-label-md flex items-center gap-1 hover:bg-surface-container transition-colors">
<span class="material-symbols-outlined text-[18px]">filter_list</span> 필터
</button>
<button class="px-3 py-1.5 bg-white border border-outline-variant rounded-md text-label-md flex items-center gap-1 hover:bg-surface-container transition-colors">
<span class="material-symbols-outlined text-[18px]">download</span> 엑셀 다운로드
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead class="bg-surface-container-high border-b border-outline-variant">
<tr>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">이름</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">유형</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">소속</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">등록일</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">체크인</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">배지</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant"></th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant">
<!-- Row 1 -->
<tr class="hover:bg-surface-container transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-primary-fixed text-on-primary-fixed flex items-center justify-center font-bold text-label-md">K</div>
<span class="font-medium text-body-base">김*희</span>
</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 bg-primary-fixed text-on-primary-fixed-variant rounded-full text-[11px] font-bold">관람객</span>
</td>
<td class="px-6 py-4 text-body-sm text-on-surface-variant">LG 전자</td>
<td class="px-6 py-4 text-body-sm text-outline">2024.10.12</td>
<td class="px-6 py-4">
<span class="inline-flex items-center gap-1.5 text-tertiary font-bold text-body-sm">
<span class="w-2 h-2 rounded-full bg-tertiary"></span> 완료
</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary">qr_code</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-outline cursor-pointer">more_vert</span>
</td>
</tr>
<!-- Row 2 -->
<tr class="bg-surface-container-low hover:bg-surface-container transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-secondary-fixed text-on-secondary-fixed flex items-center justify-center font-bold text-label-md">L</div>
<span class="font-medium text-body-base">이*준</span>
</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 bg-secondary-fixed text-on-secondary-fixed-variant rounded-full text-[11px] font-bold">바이어</span>
</td>
<td class="px-6 py-4 text-body-sm text-on-surface-variant">Samsung Electronics</td>
<td class="px-6 py-4 text-body-sm text-outline">2024.10.14</td>
<td class="px-6 py-4">
<span class="inline-flex items-center gap-1.5 text-outline font-bold text-body-sm">
<span class="w-2 h-2 rounded-full bg-outline"></span> 미입장
</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary">qr_code</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-outline cursor-pointer">more_vert</span>
</td>
</tr>
<!-- Row 3 -->
<tr class="hover:bg-surface-container transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-tertiary-fixed text-on-tertiary-fixed flex items-center justify-center font-bold text-label-md">P</div>
<span class="font-medium text-body-base">박*서</span>
</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 bg-tertiary-fixed text-on-tertiary-fixed-variant rounded-full text-[11px] font-bold">VIP</span>
</td>
<td class="px-6 py-4 text-body-sm text-on-surface-variant">Hyundai Motors</td>
<td class="px-6 py-4 text-body-sm text-outline">2024.10.15</td>
<td class="px-6 py-4">
<span class="inline-flex items-center gap-1.5 text-tertiary font-bold text-body-sm">
<span class="w-2 h-2 rounded-full bg-tertiary"></span> 완료
</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary">qr_code</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-outline cursor-pointer">more_vert</span>
</td>
</tr>
<!-- Row 4 -->
<tr class="bg-surface-container-low hover:bg-surface-container transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-error-container text-on-error-container flex items-center justify-center font-bold text-label-md">C</div>
<span class="font-medium text-body-base">최*훈</span>
</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 bg-primary-fixed text-on-primary-fixed-variant rounded-full text-[11px] font-bold">관람객</span>
</td>
<td class="px-6 py-4 text-body-sm text-on-surface-variant">개인 참관</td>
<td class="px-6 py-4 text-body-sm text-outline">2024.10.15</td>
<td class="px-6 py-4">
<span class="inline-flex items-center gap-1.5 text-error font-bold text-body-sm">
<span class="w-2 h-2 rounded-full bg-error"></span> 취소
</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-outline opacity-20">qr_code</span>
</td>
<td class="px-6 py-4">
<span class="material-symbols-outlined text-outline cursor-pointer">more_vert</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="px-stack-lg py-stack-md flex items-center justify-between bg-surface-container-lowest">
<p class="text-body-sm text-outline">총 12,480명 중 1-4 표시 중</p>
<div class="flex gap-2">
<button class="p-1.5 border border-outline-variant rounded-md hover:bg-surface-container"><span class="material-symbols-outlined text-[18px]">chevron_left</span></button>
<button class="px-3 py-1.5 bg-primary text-on-primary rounded-md text-label-md">1</button>
<button class="px-3 py-1.5 border border-outline-variant rounded-md text-label-md hover:bg-surface-container">2</button>
<button class="px-3 py-1.5 border border-outline-variant rounded-md text-label-md hover:bg-surface-container">3</button>
<button class="p-1.5 border border-outline-variant rounded-md hover:bg-surface-container"><span class="material-symbols-outlined text-[18px]">chevron_right</span></button>
</div>
</div>
</section>
</main>
<script>
// Micro-interactions and effects
document.querySelectorAll('.hover\\:scale-95').forEach(button => {
button.addEventListener('mousedown', () => button.classList.add('scale-95'));
button.addEventListener('mouseup', () => button.classList.remove('scale-95'));
button.addEventListener('mouseleave', () => button.classList.remove('scale-95'));
});
// Simple tab/active logic for demonstration
const navItems = document.querySelectorAll('nav > div');
navItems.forEach(item => {
item.addEventListener('click', () => {
navItems.forEach(i => {
i.classList.remove('bg-secondary-container', 'text-on-secondary-container', 'font-bold', 'border-l-4', 'border-secondary');
i.classList.add('text-surface-variant', 'hover:bg-surface-container-highest/10');
});
item.classList.add('bg-secondary-container', 'text-on-secondary-container', 'font-bold', 'border-l-4', 'border-secondary');
item.classList.remove('text-surface-variant', 'hover:bg-surface-container-highest/10');
});
});
</script>
</body></html>

View File

@ -1,504 +0,0 @@
<!DOCTYPE html>
<html lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - 리드 관리 시스템 (SCR-32)</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-container-highest": "#dbe2fa",
"surface-tint": "#24609e",
"primary": "#003765",
"surface-bright": "#faf8ff",
"tertiary-fixed": "#8ef7c4",
"outline": "#727781",
"surface-dim": "#d3d9f1",
"secondary-fixed": "#e5deff",
"background": "#faf8ff",
"tertiary": "#003e28",
"on-primary": "#ffffff",
"outline-variant": "#c1c7d3",
"on-primary-container": "#91c0ff",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f3ff",
"on-primary-fixed-variant": "#004881",
"secondary-fixed-dim": "#c9bfff",
"on-secondary-container": "#f3edff",
"surface": "#faf8ff",
"on-secondary-fixed": "#1b0063",
"primary-fixed-dim": "#a2c9ff",
"surface-variant": "#dbe2fa",
"secondary-container": "#6d4aff",
"secondary": "#5427e6",
"on-tertiary-fixed": "#002113",
"tertiary-fixed-dim": "#72daa9",
"on-tertiary": "#ffffff",
"inverse-on-surface": "#eef0ff",
"on-error": "#ffffff",
"error": "#ba1a1a",
"on-background": "#141b2c",
"error-container": "#ffdad6",
"on-secondary": "#ffffff",
"primary-fixed": "#d3e4ff",
"on-tertiary-fixed-variant": "#005236",
"error-red": "#ba1a1a",
"on-secondary-fixed-variant": "#4500d8",
"inverse-surface": "#293042",
"on-primary-fixed": "#001c38",
"inverse-primary": "#a2c9ff",
"primary-container": "#004e8b",
"on-surface-variant": "#424750",
"tertiary-container": "#00583b",
"surface-container-high": "#e1e7ff",
"on-error-container": "#93000a",
"on-surface": "#141b2c",
"surface-container": "#eaedff",
"on-tertiary-container": "#68d1a0"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "24px",
"section-padding": "96px",
"stack-md": "16px",
"container-padding": "24px",
"gutter": "16px",
"base": "4px",
"stack-sm": "8px"
},
"fontFamily": {
"headline-md": ["Inter"],
"display-lg": ["Inter"],
"label-xs": ["Geist"],
"label-md": ["Geist"],
"body-base": ["Inter"],
"title-sm": ["Inter"],
"body-sm": ["Inter"]
},
"fontSize": {
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.active-nav-border {
border-left: 4px solid #5427e6;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
</style>
</head>
<body class="bg-surface font-body-base text-on-surface">
<!-- Sidebar Navigation -->
<aside class="h-screen w-64 fixed left-0 top-0 bg-surface-container-low border-r border-outline-variant flex flex-col py-6 z-50">
<div class="px-6 mb-10">
<h1 class="font-display-lg text-display-lg font-bold text-primary">KINTEX AI</h1>
<p class="font-label-md text-label-md text-on-surface-variant opacity-70">전시관리 시스템</p>
</div>
<nav class="flex-grow space-y-1">
<a class="flex items-center px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">dashboard</span> 대시보드
</a>
<a class="flex items-center px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">event</span> 전시 관리
</a>
<a class="flex items-center px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">group</span> 사용자 관리
</a>
<a class="flex items-center px-6 py-3 bg-secondary-container text-on-secondary-container font-bold active-nav-border font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">person_search</span> 리드 관리
</a>
<a class="flex items-center px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">sensors</span> 기기 모니터링
</a>
<a class="flex items-center px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">leaderboard</span> 통계 분석
</a>
<a class="flex items-center px-6 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">history</span> 감사 로그
</a>
</nav>
<div class="px-6 mt-auto pt-6 border-t border-outline-variant">
<a class="flex items-center py-2 text-on-surface-variant hover:underline font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">help</span> 도움말
</a>
<a class="flex items-center py-2 text-on-surface-variant hover:underline font-label-md text-label-md" href="#">
<span class="material-symbols-outlined mr-3">logout</span> 로그아웃
</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-64 min-h-screen flex flex-col">
<!-- TopAppBar -->
<header class="sticky top-0 z-40 bg-surface h-16 border-b border-outline-variant flex items-center justify-between px-container-padding">
<div class="flex items-center gap-4">
<h2 class="font-headline-md text-headline-md font-bold text-on-surface">리드 관리 (SCR-32)</h2>
<div class="relative w-80">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]">search</span>
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm focus:ring-2 focus:ring-primary focus:border-transparent outline-none" placeholder="리드 이름 또는 회사명 검색..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4">
<button class="bg-primary text-on-primary px-4 py-2 rounded-lg font-label-md text-label-md hover:bg-primary-container transition-colors">
CSV 내보내기
</button>
<div class="flex items-center gap-2">
<button class="p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors relative">
<span class="material-symbols-outlined">notifications</span>
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full"></span>
</button>
<button class="p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors">
<span class="material-symbols-outlined">settings</span>
</button>
<div class="w-8 h-8 rounded-full overflow-hidden border border-outline-variant">
<img class="w-full h-full object-cover" data-alt="A professional headshot of an Asian male executive in a well-tailored charcoal grey suit and blue silk tie. He is smiling warmly and confidently against a neutral studio background. The lighting is soft and professional, emphasizing his clear skin and neat hairstyle, perfectly matching a high-end corporate AI dashboard aesthetic." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDXZ_Ne7Cy5TEBHWgOVg7xzVY9c_JZuql6sEpRDUNhm4SCxvRm88AvP8ttFDxTt8muPvPXfPxyUDa4dJs0sURlvjofOegNJNnCiB3cdQYLS4C028Ord3axlxg9aqYbsbyUgOS_JjZi0Qpz5qASJYVL_p8RUgBDxcZcHyd3OPnnOYLN_dEWmh8IE2WPFCTKPyo6beaVYLgNbVsDwmY1YHHTi32LuITcmFb3Ph2ArxNQr2Tej78s-AA681FK0A-G-bWC9M7xmgseOX5Q"/>
</div>
</div>
</div>
</header>
<div class="p-container-padding space-y-stack-lg">
<!-- KPI Section -->
<section class="grid grid-cols-1 md:grid-cols-4 gap-gutter">
<div class="bg-surface-container-lowest border border-outline-variant p-stack-md rounded-xl">
<p class="font-label-md text-label-md text-on-surface-variant mb-1">총 리드</p>
<div class="flex items-end justify-between">
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">342</h3>
<span class="text-secondary text-[24px] material-symbols-outlined">groups</span>
</div>
</div>
<div class="bg-surface-container-lowest border-l-4 border-secondary border-outline-variant p-stack-md rounded-xl shadow-md">
<p class="font-label-md text-label-md text-secondary font-bold mb-1">핫리드</p>
<div class="flex items-end justify-between">
<h3 class="font-headline-md text-headline-md font-bold text-secondary">68</h3>
<span class="text-secondary text-[24px] material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">local_fire_department</span>
</div>
</div>
<div class="bg-surface-container-lowest border border-outline-variant p-stack-md rounded-xl">
<p class="font-label-md text-label-md text-on-surface-variant mb-1">팔로업 대기</p>
<div class="flex items-end justify-between">
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">120</h3>
<span class="text-on-surface-variant text-[24px] material-symbols-outlined">pending_actions</span>
</div>
</div>
<div class="bg-surface-container-lowest border border-outline-variant p-stack-md rounded-xl flex items-center justify-between">
<div>
<p class="font-label-md text-label-md text-on-surface-variant mb-1">전환 추정</p>
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">24</h3>
</div>
<img alt="AI Sparkle" class="w-12 h-12" src="https://lh3.googleusercontent.com/aida/AP1WRLu1SFz3uW8s6iC8BUAS9XvAFoM_7LQZUzR-UgorcHCSIZ_uRCF-ZTLQnQbNUG13WdRaQDX_3m0ewu6jxJ2Ie-gQvwFXyYdy3ZIxhkl0hJ2VGRZF6Iul1GgmSKzO5x92HASTCa0m0XzXjctyxy5aRgxOTOxhzl-Qz9P5jTlRzXngIWh6bCNZdaVEIxd9H6a2fDdToHYg0gowaU93SV2P5YbT0-8ShD0eeNiQEFHXn4HcsQUCrsiacEeGDZY"/>
</div>
</section>
<!-- Main Layout Grid -->
<div class="grid grid-cols-3 gap-gutter">
<!-- Left: Leads Table -->
<section class="col-span-2 space-y-stack-md">
<div class="flex items-center gap-2 overflow-x-auto pb-2">
<span class="font-label-md text-label-md text-on-surface-variant mr-2">필터:</span>
<div class="bg-primary-fixed text-on-primary-fixed-variant px-3 py-1 rounded-full font-label-md text-label-md flex items-center gap-1 cursor-pointer">
스코어 80+ <span class="material-symbols-outlined text-[14px]">close</span>
</div>
<div class="bg-surface-container-high text-on-surface-variant px-3 py-1 rounded-full font-label-md text-label-md flex items-center gap-1 cursor-pointer hover:bg-surface-container-highest">
로봇 <span class="material-symbols-outlined text-[14px]">close</span>
</div>
<div class="bg-surface-container-high text-on-surface-variant px-3 py-1 rounded-full font-label-md text-label-md flex items-center gap-1 cursor-pointer hover:bg-surface-container-highest">
솔루션 <span class="material-symbols-outlined text-[14px]">close</span>
</div>
<button class="text-primary font-label-md text-label-md flex items-center hover:underline ml-auto">
<span class="material-symbols-outlined text-[18px] mr-1">tune</span> 상세 필터
</button>
</div>
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden">
<table class="w-full text-left border-collapse">
<thead class="bg-surface-container-low border-b border-outline-variant">
<tr>
<th class="px-4 py-3 font-label-md text-label-md text-on-surface-variant">이름</th>
<th class="px-4 py-3 font-label-md text-label-md text-on-surface-variant">소속</th>
<th class="px-4 py-3 font-label-md text-label-md text-on-surface-variant">관심 제품</th>
<th class="px-4 py-3 font-label-md text-label-md text-on-surface-variant text-center">관심도</th>
<th class="px-4 py-3 font-label-md text-label-md text-on-surface-variant">AI 스코어</th>
<th class="px-4 py-3 font-label-md text-label-md text-on-surface-variant">수집시각</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant">
<!-- Selected Row -->
<tr class="bg-secondary-container/10 border-l-4 border-secondary cursor-pointer">
<td class="px-4 py-4 font-body-base text-body-base font-bold text-on-surface">김**</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">(주)테크솔루션</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">협동로봇 HR-A1</td>
<td class="px-4 py-4 text-center">
<div class="flex justify-center text-secondary">
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
</div>
</td>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<div class="flex-grow bg-surface-container-high h-2 rounded-full overflow-hidden">
<div class="bg-secondary h-full rounded-full" style="width: 87%"></div>
</div>
<span class="bg-secondary text-on-secondary px-2 py-0.5 rounded-full font-label-xs text-label-xs">87</span>
</div>
</td>
<td class="px-4 py-4 font-body-sm text-body-sm text-on-surface-variant">14:22:05</td>
</tr>
<!-- Other Rows -->
<tr class="hover:bg-surface-container-low transition-colors cursor-pointer">
<td class="px-4 py-4 font-body-base text-body-base text-on-surface">이**</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">넥스트 이노베이션</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">스마트 물류 시스템</td>
<td class="px-4 py-4 text-center">
<div class="flex justify-center text-outline">
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
</div>
</td>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<div class="flex-grow bg-surface-container-high h-2 rounded-full overflow-hidden">
<div class="bg-primary h-full rounded-full" style="width: 65%"></div>
</div>
<span class="bg-primary text-on-primary px-2 py-0.5 rounded-full font-label-xs text-label-xs">65</span>
</div>
</td>
<td class="px-4 py-4 font-body-sm text-body-sm text-on-surface-variant">14:15:30</td>
</tr>
<tr class="hover:bg-surface-container-low transition-colors cursor-pointer">
<td class="px-4 py-4 font-body-base text-body-base text-on-surface">박**</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">미래 제조 연구소</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">AI 비전 검사 모듈</td>
<td class="px-4 py-4 text-center">
<div class="flex justify-center text-outline">
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
</div>
</td>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<div class="flex-grow bg-surface-container-high h-2 rounded-full overflow-hidden">
<div class="bg-secondary h-full rounded-full" style="width: 82%"></div>
</div>
<span class="bg-secondary text-on-secondary px-2 py-0.5 rounded-full font-label-xs text-label-xs">82</span>
</div>
</td>
<td class="px-4 py-4 font-body-sm text-body-sm text-on-surface-variant">14:02:11</td>
</tr>
<tr class="hover:bg-surface-container-low transition-colors cursor-pointer">
<td class="px-4 py-4 font-body-base text-body-base text-on-surface">정**</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">글로벌 테크</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">자율주행 AMR</td>
<td class="px-4 py-4 text-center">
<div class="flex justify-center text-outline">
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
</div>
</td>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<div class="flex-grow bg-surface-container-high h-2 rounded-full overflow-hidden">
<div class="bg-primary h-full rounded-full" style="width: 44%"></div>
</div>
<span class="bg-primary text-on-primary px-2 py-0.5 rounded-full font-label-xs text-label-xs">44</span>
</div>
</td>
<td class="px-4 py-4 font-body-sm text-body-sm text-on-surface-variant">13:55:42</td>
</tr>
<tr class="hover:bg-surface-container-low transition-colors cursor-pointer">
<td class="px-4 py-4 font-body-base text-body-base text-on-surface">강**</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">SCT 시스템</td>
<td class="px-4 py-4 font-body-base text-body-base text-on-surface-variant">클라우드 모니터링</td>
<td class="px-4 py-4 text-center">
<div class="flex justify-center text-outline">
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
<span class="material-symbols-outlined text-[18px]">star</span>
</div>
</td>
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<div class="flex-grow bg-surface-container-high h-2 rounded-full overflow-hidden">
<div class="bg-primary h-full rounded-full" style="width: 28%"></div>
</div>
<span class="bg-primary text-on-primary px-2 py-0.5 rounded-full font-label-xs text-label-xs">28</span>
</div>
</td>
<td class="px-4 py-4 font-body-sm text-body-sm text-on-surface-variant">13:48:19</td>
</tr>
</tbody>
</table>
<div class="p-4 bg-surface-container-low flex items-center justify-between">
<span class="font-label-md text-label-md text-on-surface-variant">총 342건 중 1-5건 표시</span>
<div class="flex items-center gap-2">
<button class="p-1 rounded-lg border border-outline-variant hover:bg-surface-container-high disabled:opacity-50" disabled="">
<span class="material-symbols-outlined">chevron_left</span>
</button>
<button class="w-8 h-8 rounded-lg bg-primary text-on-primary font-label-md text-label-md">1</button>
<button class="w-8 h-8 rounded-lg hover:bg-surface-container-high font-label-md text-label-md">2</button>
<button class="w-8 h-8 rounded-lg hover:bg-surface-container-high font-label-md text-label-md">3</button>
<button class="p-1 rounded-lg border border-outline-variant hover:bg-surface-container-high">
<span class="material-symbols-outlined">chevron_right</span>
</button>
</div>
</div>
</div>
</section>
<!-- Right: Lead Detail Panel -->
<aside class="space-y-stack-md">
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-stack-lg shadow-sm glass-panel">
<!-- Top Profile -->
<div class="flex items-center gap-4 mb-6">
<div class="w-16 h-16 rounded-full overflow-hidden border-2 border-secondary p-0.5">
<img class="w-full h-full object-cover rounded-full" data-alt="A professional close-up photo of a middle-aged Asian man in a business suit, representing a purchasing manager. He looks friendly and professional with a gentle smile. The background is a blurred office environment with soft, natural light coming from a window, emphasizing a high-end corporate B2B feel." src="https://lh3.googleusercontent.com/aida-public/AB6AXuADcrOSaD2ZKnncz3sAJ_w6eX2njSIckDTojxtGsknJV2cj5TXyZz4DCSQNfg1O20MDIEpOwZ4ErBYZAqdUtIKT_HX_rMukezwsajrc_p4i4P3s4R0Q8gTJuxTznPB2MsqU0Atn_HTWAIwX547X9wJm5yn9fteF4bWDooroO2vFkccZ41lE0ljMhPcvW4JbjYX7RJc3j4JbYIHPPBlQzHba_Yd5Qka1Nx4WoL_MNkzlJVKyMAYOF3lUgCRknLGwHYaeP0FTbwEJMsM"/>
</div>
<div>
<div class="flex items-center gap-2">
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">김**</h3>
<span class="bg-secondary-fixed text-on-secondary-fixed font-label-xs text-label-xs px-2 py-0.5 rounded">HOT</span>
</div>
<p class="font-body-base text-body-base text-on-surface-variant">구매 담당자 | (주)테크솔루션</p>
</div>
</div>
<!-- AI Score Card -->
<div class="bg-secondary-container/5 border border-secondary rounded-xl p-stack-md mb-6 relative overflow-hidden">
<div class="absolute top-0 right-0 p-2 opacity-10">
<span class="material-symbols-outlined text-[64px]">psychology</span>
</div>
<div class="flex items-center justify-between mb-4">
<h4 class="font-title-sm text-title-sm font-bold text-secondary">AI 스코어 분석</h4>
<span class="text-secondary font-display-lg text-display-lg font-bold">87</span>
</div>
<ul class="space-y-2 font-body-sm text-body-sm text-on-surface">
<li class="flex items-start gap-2">
<span class="material-symbols-outlined text-secondary text-[18px]">check_circle</span>
<span>과거 유사 전시회 로봇 구매 이력 있음</span>
</li>
<li class="flex items-start gap-2">
<span class="material-symbols-outlined text-secondary text-[18px]">check_circle</span>
<span>당사 부스 체류 시간 평균 대비 240% 높음</span>
</li>
<li class="flex items-start gap-2">
<span class="material-symbols-outlined text-secondary text-[18px]">check_circle</span>
<span>기술 사양서(PDF) 다운로드 2회 기록</span>
</li>
</ul>
</div>
<!-- Follow-up Section -->
<div class="space-y-stack-sm mb-8">
<h4 class="font-label-md text-label-md font-bold text-on-surface flex items-center gap-2">
<span class="material-symbols-outlined text-secondary text-[18px]">auto_awesome</span>
AI 추천 팔로업 메시지
</h4>
<div class="relative">
<textarea class="w-full bg-surface-container-low border border-outline-variant rounded-lg p-3 text-body-sm min-h-[120px] focus:ring-primary focus:border-primary outline-none">안녕하세요, (주)테크솔루션 김** 팀장님.
KINTEX AI 2045 부스에 방문해주셔서 대단히 감사합니다. 관심을 보이셨던 '협동로봇 HR-A1'의 상세 기술 사양과 귀사의 공정 최적화를 위한 커스텀 제안서를 준비했습니다.
다음 주 중 편하신 시간에 짧은 미팅이 가능하실까요?</textarea>
<button class="absolute bottom-2 right-2 p-1.5 bg-surface-container-highest text-secondary rounded-md hover:bg-secondary-fixed transition-colors">
<span class="material-symbols-outlined text-[18px]">refresh</span>
</button>
</div>
</div>
<!-- Actions -->
<div class="space-y-3">
<button class="w-full bg-primary text-on-primary py-3 rounded-xl font-title-sm text-title-sm font-bold hover:bg-primary-container transition-all flex items-center justify-center gap-2 shadow-md">
<span class="material-symbols-outlined">send</span>
EDM 발송하기
</button>
<div class="grid grid-cols-2 gap-3">
<button class="py-2.5 rounded-lg border border-outline-variant font-label-md text-label-md text-on-surface-variant hover:bg-surface-container-high transition-colors flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">download</span>
CSV 내보내기
</button>
<button class="py-2.5 rounded-lg border border-outline-variant font-label-md text-label-md text-on-surface-variant hover:bg-surface-container-high transition-colors flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">edit_note</span>
노트 기록
</button>
</div>
</div>
</div>
<!-- Additional Context (Optional Activity Feed) -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-stack-md">
<h4 class="font-label-md text-label-md font-bold text-on-surface mb-3">최근 활동 로그</h4>
<div class="space-y-3">
<div class="flex gap-3 items-start">
<div class="w-1 h-1 rounded-full bg-secondary mt-2"></div>
<div>
<p class="text-body-sm text-on-surface">제품 카탈로그 QR 스캔</p>
<span class="text-label-xs text-on-surface-variant">오늘 14:22</span>
</div>
</div>
<div class="flex gap-3 items-start">
<div class="w-1 h-1 rounded-full bg-secondary mt-2"></div>
<div>
<p class="text-body-sm text-on-surface">부스 내 HR-A1 구동 시연 참관</p>
<span class="text-label-xs text-on-surface-variant">오늘 14:15</span>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
<!-- Footer -->
<footer class="mt-auto bg-surface-container-lowest border-t border-outline-variant py-4 px-container-padding flex justify-between items-center">
<span class="font-label-xs text-label-xs text-on-surface-variant">© 2024 KINTEX AI 전시관리. All rights reserved.</span>
<div class="flex gap-6">
<a class="font-label-xs text-label-xs text-on-surface-variant hover:underline" href="#">Privacy Policy</a>
<a class="font-label-xs text-label-xs text-on-surface-variant hover:underline" href="#">Terms of Service</a>
<a class="font-label-xs text-label-xs text-on-surface-variant hover:underline" href="#">Contact Support</a>
</div>
</footer>
</main>
<script>
// Simple micro-interaction for rows
document.querySelectorAll('tbody tr').forEach(row => {
row.addEventListener('click', () => {
document.querySelectorAll('tbody tr').forEach(r => {
r.classList.remove('bg-secondary-container/10', 'border-l-4', 'border-secondary');
r.classList.add('hover:bg-surface-container-low');
});
row.classList.add('bg-secondary-container/10', 'border-l-4', 'border-secondary');
row.classList.remove('hover:bg-surface-container-low');
});
});
</script>
</body></html>

View File

@ -1,497 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI System - EDM 캠페인 관리</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
vertical-align: middle;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
body {
background-color: #f9f9ff;
color: #141b2c;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #c2c7d1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #727781; }
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-primary": "#a2c9ff",
"surface-container-low": "#f2f3ff",
"outline": "#727781",
"on-primary": "#ffffff",
"error": "#ba1a1a",
"tertiary-fixed": "#8ef7c4",
"inverse-on-surface": "#eef0ff",
"secondary": "#5427e6",
"on-primary-fixed-variant": "#004881",
"primary-fixed-dim": "#a2c9ff",
"on-error-container": "#93000a",
"on-tertiary-fixed": "#002113",
"outline-variant": "#c2c7d1",
"secondary-fixed-dim": "#c9bfff",
"primary-fixed": "#d3e4ff",
"on-background": "#141b2c",
"surface-bright": "#f9f9ff",
"primary-container": "#004e8b",
"surface-container-lowest": "#ffffff",
"surface-container-high": "#e1e7ff",
"background": "#faf8ff",
"on-tertiary-fixed-variant": "#005236",
"on-primary-fixed": "#001c38",
"on-secondary-container": "#f3edff",
"on-surface": "#141b2c",
"surface-container-highest": "#dbe2fa",
"surface-container": "#eaedff",
"primary": "#003765",
"error-container": "#ffdad6",
"on-primary-container": "#91c0ff",
"surface-tint": "#24609e",
"on-tertiary": "#ffffff",
"secondary-container": "#6d4aff",
"on-tertiary-container": "#68d1a0",
"inverse-surface": "#293042",
"tertiary-container": "#00583b",
"on-surface-variant": "#424750",
"error-red": "#ba1a1a",
"tertiary": "#003e28",
"tertiary-fixed-dim": "#72daa9",
"on-secondary-fixed": "#1b0063",
"surface": "#faf8ff",
"on-secondary-fixed-variant": "#4500d8",
"secondary-fixed": "#e5deff",
"on-error": "#ffffff",
"on-secondary": "#ffffff",
"surface-dim": "#d3d9f1",
"surface-variant": "#dbe2fa"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"base": "4px",
"container-padding": "24px",
"gutter": "16px",
"stack-md": "16px",
"stack-lg": "24px",
"stack-sm": "8px",
"section-padding": "96px"
},
"fontFamily": {
"title-sm": ["Inter"],
"display-lg": ["Inter"],
"label-md": ["Geist"],
"label-xs": ["Geist"],
"body-sm": ["Inter"],
"display-lg-mobile": ["Inter"],
"body-base": ["Inter"],
"headline-md": ["Inter"]
},
"fontSize": {
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}]
}
},
},
}
</script>
</head>
<body class="font-body-base overflow-x-hidden">
<!-- SideNavBar Shell -->
<aside class="w-[240px] h-screen fixed left-0 top-0 bg-inverse-surface dark:bg-on-surface flex flex-col py-stack-lg px-gutter border-r border-outline-variant z-50">
<div class="mb-10">
<h1 class="font-display-lg text-display-lg font-black text-on-primary-fixed tracking-tight">KINTEX</h1>
<p class="font-label-md text-label-md text-surface-variant opacity-70">글로벌 마이스 플랫폼</p>
</div>
<nav class="flex-1 space-y-2">
<!-- Active Tab: Using '마케팅' equivalent context (Applying logic: '대시보드' is default, but prompt asks for marketing active) -->
<!-- Note: Mapping labels from JSON: ["대시보드", "관람객", "전시 관리", "신청 폼", "통계 분석", "시스템 설정"] -->
<a class="flex items-center gap-3 py-3 px-4 rounded-lg bg-secondary-container text-on-secondary-container font-bold border-l-4 border-secondary transition-colors duration-200" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">campaign</span>
<span class="font-label-md text-label-md">마케팅 관리</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-lg text-surface-variant hover:text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-200" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">대시보드</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-lg text-surface-variant hover:text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-200" href="#">
<span class="material-symbols-outlined">group</span>
<span class="font-label-md text-label-md">관람객</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-lg text-surface-variant hover:text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-200" href="#">
<span class="material-symbols-outlined">event_available</span>
<span class="font-label-md text-label-md">전시 관리</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-lg text-surface-variant hover:text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-200" href="#">
<span class="material-symbols-outlined">analytics</span>
<span class="font-label-md text-label-md">통계 분석</span>
</a>
</nav>
<div class="mt-auto space-y-2">
<button class="w-full mb-4 bg-primary text-on-primary py-2 rounded-lg font-label-md text-label-md hover:opacity-90 transition-all">
AI 분석 리포트
</button>
<a class="flex items-center gap-3 py-2 px-4 text-surface-variant hover:text-on-surface-variant transition-colors" href="#">
<span class="material-symbols-outlined">help</span>
<span class="font-label-md text-label-md">헬프센터</span>
</a>
<a class="flex items-center gap-3 py-2 px-4 text-surface-variant hover:text-on-surface-variant transition-colors" href="#">
<span class="material-symbols-outlined">logout</span>
<span class="font-label-md text-label-md">로그아웃</span>
</a>
</div>
</aside>
<!-- TopAppBar Shell -->
<header class="fixed top-0 left-[240px] w-[calc(100%-240px)] h-16 flex justify-between items-center px-container-padding bg-surface-bright border-b border-outline-variant z-40">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary cursor-pointer">menu_open</span>
<h2 class="font-headline-md text-headline-md text-primary">EDM 캠페인 관리</h2>
</div>
<div class="flex items-center gap-6">
<div class="relative w-64">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline-variant">search</span>
<input class="w-full pl-10 pr-4 py-1.5 rounded-full border border-outline-variant bg-surface-container-low focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all font-body-sm text-body-sm" placeholder="캠페인 검색..." type="text"/>
</div>
<div class="flex items-center gap-4 text-on-surface-variant">
<span class="material-symbols-outlined cursor-pointer hover:text-primary">notifications</span>
<span class="material-symbols-outlined cursor-pointer hover:text-primary">security</span>
<div class="flex items-center gap-2 border-l border-outline-variant pl-4 cursor-pointer">
<span class="material-symbols-outlined">account_circle</span>
<span class="font-label-md text-label-md text-on-surface">KINTEX Admin</span>
</div>
</div>
</div>
</header>
<!-- Main Canvas -->
<main class="ml-[240px] pt-24 px-container-padding pb-stack-lg min-h-screen">
<!-- KPI Section -->
<section class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-stack-lg">
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-primary shadow-sm">
<p class="font-label-md text-label-md text-outline mb-1">전체 발송</p>
<h3 class="font-display-lg text-display-lg text-primary tracking-tight">24,000<span class="text-title-sm ml-1"></span></h3>
<div class="mt-2 text-label-xs text-secondary flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">trending_up</span>
<span>지난달 대비 12% 증가</span>
</div>
</div>
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-secondary shadow-sm">
<p class="font-label-md text-label-md text-outline mb-1">오픈율</p>
<h3 class="font-display-lg text-display-lg text-secondary tracking-tight">38<span class="text-title-sm ml-1">%</span></h3>
<div class="mt-2 text-label-xs text-on-tertiary-fixed-variant flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">check_circle</span>
<span>산업군 평균 대비 우수</span>
</div>
</div>
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-tertiary shadow-sm">
<p class="font-label-md text-label-md text-outline mb-1">클릭율</p>
<h3 class="font-display-lg text-display-lg text-tertiary-container tracking-tight">9<span class="text-title-sm ml-1">%</span></h3>
<div class="mt-2 text-label-xs text-outline flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">history</span>
<span>유지 중</span>
</div>
</div>
<div class="bg-surface-container-lowest p-stack-md rounded-xl border border-outline-variant border-l-4 border-error shadow-sm">
<p class="font-label-md text-label-md text-outline mb-1">등록전환</p>
<h3 class="font-display-lg text-display-lg text-error tracking-tight">1,240<span class="text-title-sm ml-1"></span></h3>
<div class="mt-2 text-label-xs text-error flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">bolt</span>
<span>실시간 목표 달성 82%</span>
</div>
</div>
</section>
<!-- Main Action Row -->
<div class="flex justify-between items-center mb-stack-md">
<div class="flex items-center gap-4">
<h4 class="font-title-sm text-title-sm">캠페인 목록</h4>
<div class="flex gap-2">
<span class="px-3 py-1 rounded-full bg-surface-container-high text-on-surface-variant font-label-md text-[11px] cursor-pointer">전체</span>
<span class="px-3 py-1 rounded-full bg-primary-fixed text-on-primary-fixed-variant font-label-md text-[11px] cursor-pointer">진행중</span>
<span class="px-3 py-1 rounded-full bg-surface-container-high text-on-surface-variant font-label-md text-[11px] cursor-pointer">완료</span>
</div>
</div>
<button class="bg-primary text-on-primary flex items-center gap-2 px-6 py-2 rounded-lg font-label-md text-label-md hover:bg-primary-container transition-all active:scale-95 shadow-md">
<span class="material-symbols-outlined">add</span>
새 캠페인
</button>
</div>
<!-- Content Grid (2:1 Layout) -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
<!-- Left Column: Campaign List -->
<div class="lg:col-span-2 space-y-4">
<!-- List Row 1 -->
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant flex items-center justify-between hover:shadow-md transition-all group">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-lg bg-primary-fixed flex items-center justify-center text-primary">
<span class="material-symbols-outlined">mail</span>
</div>
<div>
<div class="flex items-center gap-2 mb-1">
<span class="bg-surface-container-highest text-outline text-[11px] px-2 py-0.5 rounded font-bold">초안</span>
<h5 class="font-title-sm text-body-base font-bold group-hover:text-primary transition-colors">2026 스마트팩토리 사전등록 안내</h5>
</div>
<div class="flex items-center gap-3 text-on-surface-variant text-label-xs">
<span>대상: 5,200명</span>
<span class="w-1 h-1 rounded-full bg-outline-variant"></span>
<span>최종 수정: 2시간 전</span>
</div>
</div>
</div>
<div class="flex items-center gap-8">
<div class="text-center">
<p class="text-label-xs text-outline mb-1">오픈율</p>
<p class="font-bold">-</p>
</div>
<div class="text-center">
<p class="text-label-xs text-outline mb-1">클릭율</p>
<p class="font-bold">-</p>
</div>
<span class="material-symbols-outlined text-outline-variant group-hover:text-primary transition-colors cursor-pointer">chevron_right</span>
</div>
</div>
<!-- List Row 2 -->
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant flex items-center justify-between hover:shadow-md transition-all group">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-lg bg-tertiary-fixed flex items-center justify-center text-tertiary">
<span class="material-symbols-outlined">schedule</span>
</div>
<div>
<div class="flex items-center gap-2 mb-1">
<span class="bg-primary-fixed text-on-primary-fixed-variant text-[11px] px-2 py-0.5 rounded font-bold">예약</span>
<h5 class="font-title-sm text-body-base font-bold group-hover:text-primary transition-colors">VIP 바이어 네트워킹 데이 초대권</h5>
</div>
<div class="flex items-center gap-3 text-on-surface-variant text-label-xs">
<span>대상: 850명</span>
<span class="w-1 h-1 rounded-full bg-outline-variant"></span>
<span>발송예정: 2024.11.20 10:00</span>
</div>
</div>
</div>
<div class="flex items-center gap-8">
<div class="text-center">
<p class="text-label-xs text-outline mb-1">오픈율</p>
<p class="font-bold">24% <span class="text-[9px] font-normal text-outline">(예상)</span></p>
</div>
<div class="text-center">
<p class="text-label-xs text-outline mb-1">클릭율</p>
<p class="font-bold">5% <span class="text-[9px] font-normal text-outline">(예상)</span></p>
</div>
<span class="material-symbols-outlined text-outline-variant group-hover:text-primary transition-colors cursor-pointer">chevron_right</span>
</div>
</div>
<!-- List Row 3 -->
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant flex items-center justify-between hover:shadow-md transition-all group">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-lg bg-secondary-fixed flex items-center justify-center text-secondary">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">send</span>
</div>
<div>
<div class="flex items-center gap-2 mb-1">
<span class="bg-error-container text-on-error-container text-[11px] px-2 py-0.5 rounded font-bold">발송중</span>
<h5 class="font-title-sm text-body-base font-bold group-hover:text-primary transition-colors">글로벌 테크 트렌드 리포트 (Vol. 12)</h5>
</div>
<div class="flex items-center gap-3 text-on-surface-variant text-label-xs">
<span>대상: 12,000명</span>
<span class="w-1 h-1 rounded-full bg-outline-variant"></span>
<span>진행률: 68% (8,160건 완료)</span>
</div>
</div>
</div>
<div class="flex items-center gap-8">
<div class="text-center">
<p class="text-label-xs text-outline mb-1">오픈율</p>
<p class="font-bold text-secondary">42.5%</p>
</div>
<div class="text-center">
<p class="text-label-xs text-outline mb-1">클릭율</p>
<p class="font-bold text-secondary">11.2%</p>
</div>
<span class="material-symbols-outlined text-outline-variant group-hover:text-primary transition-colors cursor-pointer">chevron_right</span>
</div>
</div>
<!-- List Row 4 -->
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant flex items-center justify-between hover:shadow-md transition-all group opacity-80">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-lg bg-surface-container-high flex items-center justify-center text-outline">
<span class="material-symbols-outlined">done_all</span>
</div>
<div>
<div class="flex items-center gap-2 mb-1">
<span class="bg-on-tertiary-fixed-variant text-white text-[11px] px-2 py-0.5 rounded font-bold">완료</span>
<h5 class="font-title-sm text-body-base font-bold group-hover:text-primary transition-colors">KINTEX 추석 연휴 휴관 및 일정 안내</h5>
</div>
<div class="flex items-center gap-3 text-on-surface-variant text-label-xs">
<span>대상: 45,000명</span>
<span class="w-1 h-1 rounded-full bg-outline-variant"></span>
<span>완료일: 2024.09.12</span>
</div>
</div>
</div>
<div class="flex items-center gap-8">
<div class="text-center">
<p class="text-label-xs text-outline mb-1">오픈율</p>
<p class="font-bold">31.8%</p>
</div>
<div class="text-center">
<p class="text-label-xs text-outline mb-1">클릭율</p>
<p class="font-bold">2.4%</p>
</div>
<span class="material-symbols-outlined text-outline-variant group-hover:text-primary transition-colors cursor-pointer">chevron_right</span>
</div>
</div>
</div>
<!-- Right Column: Campaign Builder Panel -->
<aside class="space-y-gutter">
<div class="bg-white p-container-padding rounded-xl border border-outline-variant shadow-lg sticky top-24">
<div class="flex items-center justify-between mb-6">
<h4 class="font-title-sm text-title-sm flex items-center gap-2">
<img alt="AI Icon" class="w-6 h-6 object-contain" src="https://lh3.googleusercontent.com/aida/AP1WRLu1SFz3uW8s6iC8BUAS9XvAFoM_7LQZUzR-UgorcHCSIZ_uRCF-ZTLQnQbNUG13WdRaQDX_3m0ewu6jxJ2Ie-gQvwFXyYdy3ZIxhkl0hJ2VGRZF6Iul1GgmSKzO5x92HASTCa0m0XzXjctyxy5aRgxOTOxhzl-Qz9P5jTlRzXngIWh6bCNZdaVEIxd9H6a2fDdToHYg0gowaU93SV2P5YbT0-8ShD0eeNiQEFHXn4HcsQUCrsiacEeGDZY"/>
캠페인 빌더
</h4>
<span class="material-symbols-outlined text-secondary cursor-pointer">info</span>
</div>
<!-- Target Segment -->
<div class="mb-6">
<p class="font-label-md text-label-md text-on-surface mb-3">대상 세그먼트</p>
<div class="grid grid-cols-1 gap-2">
<label class="flex items-center gap-3 p-2.5 rounded-lg border border-outline-variant hover:bg-surface-container-low transition-colors cursor-pointer">
<input checked="" class="w-4 h-4 rounded border-outline-variant text-secondary focus:ring-secondary" type="checkbox"/>
<span class="text-body-sm">사전등록자 (3,240명)</span>
</label>
<label class="flex items-center gap-3 p-2.5 rounded-lg border border-outline-variant hover:bg-surface-container-low transition-colors cursor-pointer">
<input class="w-4 h-4 rounded border-outline-variant text-secondary focus:ring-secondary" type="checkbox"/>
<span class="text-body-sm">과거 관람객 (12,500명)</span>
</label>
<label class="flex items-center gap-3 p-2.5 rounded-lg border border-outline-variant hover:bg-surface-container-low transition-colors cursor-pointer">
<input class="w-4 h-4 rounded border-outline-variant text-secondary focus:ring-secondary" type="checkbox"/>
<span class="text-body-sm">바이어 리스트 (1,820명)</span>
</label>
</div>
</div>
<!-- AI Drafting -->
<div class="mb-6 p-4 bg-secondary-fixed rounded-xl border border-secondary-container/20">
<div class="flex items-center justify-between mb-3">
<span class="text-on-secondary-fixed text-label-md font-bold">AI 최적화</span>
<span class="material-symbols-outlined text-on-secondary-fixed text-[18px]" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
</div>
<p class="text-[11px] text-on-secondary-fixed-variant mb-4 leading-relaxed">세그먼트 성향을 분석하여 오픈율이 가장 높을 것으로 예상되는 카피와 발송 시간을 제안합니다.</p>
<button class="w-full bg-secondary-container text-on-secondary-container py-2.5 rounded-lg font-label-md text-label-md flex items-center justify-center gap-2 hover:opacity-90 transition-all active:scale-95">
<span class="material-symbols-outlined text-[18px]">temp_preferences_custom</span>
AI 카피 초안 생성
</button>
</div>
<!-- Image Asset Slot -->
<div class="mb-6">
<p class="font-label-md text-label-md text-on-surface mb-3">메인 이미지 에셋</p>
<div class="relative w-full aspect-video rounded-lg overflow-hidden border-2 border-dashed border-outline-variant flex items-center justify-center bg-surface-container hover:border-primary transition-colors cursor-pointer">
<img class="absolute inset-0 w-full h-full object-cover opacity-50 group-hover:opacity-70" data-alt="A futuristic, high-tech digital illustration of a nano-banana concept for a smart factory exhibition. The image features glowing neon blueprints of factory automation machinery, microscopic organic patterns in electric blue and purple, and a clean, corporate white background. The style is modern, professional, and sophisticated, reflecting global connectivity and innovation at KINTEX." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBIjd-oCn8idkfrFcL607kpiYpUYYToTAj2GlGWQrq4lJSwqRNltO6XBZKEyMMnoLs0BleDq_3Say9RAU59gjLmt6iNrYZWApCD8QR0HiuvjnEifYmel5FqGMmCM_KlNcU3_sncmWecNaj_dWHg4cXQj1iFgoUWYSlruP93Fhn-J-ZFnPqWsDJ8jlHWHy8EkkJV0dkW1tRSSh-dM7Xg1jE4S4Cg-kMLIfu5nJNQgvuEeLlhKhbFgvoP8D0UuARzfCVH4LphAFmA22I"/>
<div class="relative z-10 text-center">
<span class="material-symbols-outlined text-outline-variant text-[32px] mb-2">add_photo_alternate</span>
<p class="text-label-xs text-outline font-medium">나노바나나 예상 이미지 삽입</p>
</div>
</div>
</div>
<!-- Scheduling Toggle -->
<div class="mb-6">
<p class="font-label-md text-label-md text-on-surface mb-3">발송 설정</p>
<div class="flex gap-2 p-1 bg-surface-container rounded-lg">
<button class="flex-1 py-1.5 rounded-md bg-white text-on-surface shadow-sm font-label-md text-[12px] transition-all" id="btn-now" onclick="toggleSchedule(this)">즉시 발송</button>
<button class="flex-1 py-1.5 rounded-md text-outline font-label-md text-[12px] transition-all" id="btn-later" onclick="toggleSchedule(this)">예약 발송</button>
</div>
<div class="hidden mt-3 animate-in fade-in slide-in-from-top-2 duration-300" id="schedule-input">
<input class="w-full p-2 rounded-lg border border-outline-variant font-body-sm text-body-sm focus:ring-secondary focus:border-secondary" type="datetime-local"/>
</div>
</div>
<!-- Preview Frame -->
<div class="border border-outline-variant rounded-lg overflow-hidden">
<div class="bg-surface-container p-2 text-center border-b border-outline-variant">
<span class="text-label-xs font-bold text-outline">미리보기 (Mobile)</span>
</div>
<div class="h-48 overflow-y-auto p-4 bg-white text-[10px] space-y-3 leading-relaxed">
<div class="h-32 w-full bg-surface-container-low rounded-lg mb-2 flex items-center justify-center text-outline-variant italic">
[메인 배너 이미지 영역]
</div>
<p class="font-bold text-title-sm text-[14px] leading-tight">2026 KINTEX 스마트팩토리 엑스포에 초대합니다</p>
<p>안녕하세요, 홍길동님. KINTEX가 제안하는 미래 제조 혁신의 현장으로 귀하를 정중히 모십니다...</p>
<hr class="border-outline-variant my-4"/>
<footer class="text-outline-variant text-[8px] space-y-1">
<p>본 메일은 수신동의를 하신 고객님께 발송되었습니다.</p>
<div class="flex gap-2">
<span class="underline cursor-pointer">수신거부</span>
<span class="underline cursor-pointer">정보통신망법 수신동의 준수</span>
</div>
</footer>
</div>
</div>
<!-- Final Action -->
<button class="w-full mt-6 bg-primary text-on-primary py-3 rounded-xl font-display-lg text-title-sm shadow-lg hover:shadow-primary/20 active:scale-95 transition-all">
캠페인 실행하기
</button>
</div>
</aside>
</div>
</main>
<script>
function toggleSchedule(btn) {
const btns = [document.getElementById('btn-now'), document.getElementById('btn-later')];
const input = document.getElementById('schedule-input');
btns.forEach(b => {
b.classList.remove('bg-white', 'text-on-surface', 'shadow-sm');
b.classList.add('text-outline');
});
btn.classList.add('bg-white', 'text-on-surface', 'shadow-sm');
btn.classList.remove('text-outline');
if (btn.id === 'btn-later') {
input.classList.remove('hidden');
} else {
input.classList.add('hidden');
}
}
// Simple scale effect for interactive elements
document.querySelectorAll('button, a, .group').forEach(el => {
el.addEventListener('mousedown', () => {
el.style.transform = 'scale(0.98)';
});
el.addEventListener('mouseup', () => {
el.style.transform = '';
});
el.addEventListener('mouseleave', () => {
el.style.transform = '';
});
});
</script>
</body></html>

View File

@ -1,439 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - Sponsorship &amp; Sales Manager</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Noto+Sans+KR:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
body {
font-family: 'Inter', 'Noto Sans KR', sans-serif;
-webkit-font-smoothing: antialiased;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(193, 199, 211, 0.5);
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 10px;
}
.tier-card {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease;
}
.tier-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary": "#5427e6",
"on-tertiary": "#ffffff",
"surface-container-lowest": "#ffffff",
"primary-container": "#004e8b",
"surface-dim": "#d3d9f1",
"surface-bright": "#faf8ff",
"tertiary-fixed": "#8ef7c4",
"on-secondary": "#ffffff",
"on-error-container": "#93000a",
"inverse-on-surface": "#eef0ff",
"tertiary-container": "#00583b",
"outline-variant": "#c1c7d3",
"on-tertiary-container": "#68d1a0",
"primary-fixed-dim": "#a2c9ff",
"primary-fixed": "#d3e4ff",
"surface-container-high": "#e1e7ff",
"on-tertiary-fixed-variant": "#005236",
"on-surface": "#141b2c",
"inverse-primary": "#a2c9ff",
"surface-container-highest": "#dbe2fa",
"surface-container-low": "#f1f3ff",
"error": "#ba1a1a",
"primary": "#003765",
"on-primary-fixed": "#001c38",
"surface-tint": "#24609e",
"tertiary-fixed-dim": "#72daa9",
"background": "#faf8ff",
"secondary-fixed": "#e5deff",
"on-secondary-fixed-variant": "#4500d8",
"on-primary": "#ffffff",
"tertiary": "#003e28",
"on-primary-container": "#91c0ff",
"surface-container": "#eaedff",
"secondary-fixed-dim": "#c9bfff",
"surface-variant": "#dbe2fa",
"on-secondary-fixed": "#1b0063",
"on-tertiary-fixed": "#002113",
"secondary-container": "#6d4aff",
"on-surface-variant": "#424750",
"on-secondary-container": "#f3edff",
"error-red": "#ba1a1a",
"on-error": "#ffffff",
"surface": "#faf8ff",
"error-container": "#ffdad6",
"on-background": "#141b2c",
"inverse-surface": "#293042",
"outline": "#727781",
"on-primary-fixed-variant": "#004881"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-sm": "8px",
"stack-lg": "24px",
"gutter": "16px",
"section-padding": "96px",
"container-padding": "24px",
"base": "4px",
"stack-md": "16px"
},
"fontFamily": {
"title-sm": ["Inter", "Noto Sans KR"],
"label-xs": ["Inter"],
"label-md": ["Inter"],
"body-sm": ["Inter", "Noto Sans KR"],
"display-lg": ["Inter", "Noto Sans KR"],
"headline-md": ["Inter", "Noto Sans KR"],
"body-base": ["Inter", "Noto Sans KR"]
}
}
}
}
</script>
</head>
<body class="bg-background text-on-surface">
<!-- Side Navigation Bar -->
<aside class="fixed left-0 top-0 h-screen w-[240px] bg-surface-container-lowest border-r border-outline-variant flex flex-col py-stack-lg z-50">
<div class="px-gutter mb-10">
<img alt="KINTEX Logo" class="h-8 w-auto object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBpmxW0_W76w1IvseAoAzj5b7yfyhipKotxPrm_JIKPTc4IFprUAvfgWMXvPXtJYqM0cir2Yz1cISoFLiL7emJDpIc0HIHBb9hocUmpdHOEk5FY6ZUIDdoPv4txX9vzVKPy82xzCGbPO6ab3AzhRInSaywerIhEFzJOhT2plBFpE5-_jOlu99a8DPnF3OQ6ryQ4EwpVXKgCHGCAs59awcRxdJy2RLWhQzLbe4hSXUYXwFBtBJVBbxBfrhfd_BXif-9aZNEjn2LKKWQ"/>
<div class="mt-4">
<p class="font-headline-md text-headline-md font-bold text-primary">KINTEX Sales</p>
<p class="font-title-sm text-label-md text-on-surface-variant opacity-70">Exhibition Manager</p>
</div>
</div>
<nav class="flex-grow space-y-1">
<!-- Dashboard -->
<a class="flex items-center px-6 py-3 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined mr-3">dashboard</span>
<span class="font-label-md text-label-md">Dashboard</span>
</a>
<!-- Exhibitors -->
<a class="flex items-center px-6 py-3 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined mr-3">groups</span>
<span class="font-label-md text-label-md">Exhibitors</span>
</a>
<!-- Sponsorships (Active) -->
<a class="flex items-center px-6 py-3 text-primary font-bold border-r-4 border-primary bg-primary-container/10 transition-transform active:scale-95" href="#">
<span class="material-symbols-outlined mr-3" style="font-variation-settings: 'FILL' 1;">loyalty</span>
<span class="font-label-md text-label-md">Sponsorships</span>
</a>
<!-- Sales Pipeline -->
<a class="flex items-center px-6 py-3 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined mr-3">payments</span>
<span class="font-label-md text-label-md">Sales Pipeline</span>
</a>
<!-- Analytics -->
<a class="flex items-center px-6 py-3 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined mr-3">analytics</span>
<span class="font-label-md text-label-md">Analytics</span>
</a>
<!-- Settings -->
<a class="flex items-center px-6 py-3 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined mr-3">settings</span>
<span class="font-label-md text-label-md">Settings</span>
</a>
</nav>
<div class="px-gutter mt-auto pt-stack-lg border-t border-outline-variant space-y-1">
<button class="w-full bg-primary text-on-primary py-3 px-4 rounded-lg font-bold text-label-md flex items-center justify-center gap-2 mb-4 hover:bg-primary-container transition-all">
<span class="material-symbols-outlined text-[20px]">add_circle</span>
New Sales Lead
</button>
<a class="flex items-center px-2 py-2 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors text-label-md" href="#">
<span class="material-symbols-outlined mr-3">help</span>
Help Center
</a>
<a class="flex items-center px-2 py-2 text-on-surface-variant font-medium hover:bg-surface-container-high transition-colors text-label-md text-error" href="#">
<span class="material-symbols-outlined mr-3">logout</span>
Log Out
</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-[240px] min-h-screen flex flex-col">
<!-- Top Nav Bar -->
<header class="h-16 bg-surface-bright border-b border-outline-variant flex justify-between items-center px-gutter sticky top-0 z-40">
<div class="flex items-center gap-4">
<h1 class="font-headline-md text-headline-md font-bold text-primary">Sponsorship &amp; Sales Manager</h1>
<div class="h-6 w-[1px] bg-outline-variant"></div>
<div class="flex gap-6">
<a class="text-on-surface-variant hover:text-primary font-medium text-body-base" href="#">Q3 Report</a>
<a class="text-on-surface-variant hover:text-primary font-medium text-body-base" href="#">Sales Target</a>
<a class="text-on-surface-variant hover:text-primary font-medium text-body-base" href="#">Contact Support</a>
</div>
</div>
<div class="flex items-center gap-4">
<button class="material-symbols-outlined text-on-surface-variant p-2 rounded-full hover:bg-surface-container transition-all">notifications</button>
<button class="material-symbols-outlined text-on-surface-variant p-2 rounded-full hover:bg-surface-container transition-all">settings</button>
<div class="flex items-center gap-2 pl-2">
<img class="w-8 h-8 rounded-full border border-outline-variant" data-alt="A professional headshot of a corporate exhibition manager in their late 30s, wearing a navy blue blazer over a crisp white shirt. The background is a blurred office setting with high-end architectural details. The lighting is soft and flattering, creating a polished and trustworthy executive profile image for a business dashboard application." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCoc-dHgz8u2mHORlV01nhtu4QUoFeH4MIBmMf2sYKAxmRvL8iNDTfVcV5i08jvujHxPlqS9whVVIxKA362ca9NOByfty7q8UoWFwhGlOBtREhioMLLMp0-jmm7QvOhlF9nfINxdpcnsxihpLawNZpn0rHseHXeNnv324xqHfCQKCknzJaLRQq_kYHBuCkQNi40O72Hag89JnpZq4aO4zTSxXT0NRFAA8i4xKqygj9EGbyGM-NJlKHD0NmWf0q_bmbqFJ8C1fnY2VA"/>
<span class="text-body-base font-semibold">Admin User</span>
</div>
</div>
</header>
<!-- Page Content -->
<div class="p-8 space-y-8 max-w-[1440px] mx-auto w-full">
<!-- KPI Cards Row -->
<section class="grid grid-cols-4 gap-gutter">
<div class="bg-surface-container-lowest p-stack-lg rounded-xl border border-outline-variant flex items-center justify-between">
<div>
<p class="text-on-surface-variant font-label-md text-label-md mb-1">스폰서</p>
<p class="text-headline-md font-bold text-primary">18</p>
</div>
<div class="bg-primary-fixed p-3 rounded-full">
<span class="material-symbols-outlined text-on-primary-fixed">partner_heart</span>
</div>
</div>
<div class="bg-surface-container-lowest p-stack-lg rounded-xl border border-outline-variant flex items-center justify-between">
<div>
<p class="text-on-surface-variant font-label-md text-label-md mb-1">판매액</p>
<p class="text-headline-md font-bold text-primary">₩420.0M</p>
</div>
<div class="bg-secondary-fixed p-3 rounded-full">
<span class="material-symbols-outlined text-on-secondary-fixed">payments</span>
</div>
</div>
<div class="bg-surface-container-lowest p-stack-lg rounded-xl border border-outline-variant flex items-center justify-between">
<div>
<p class="text-on-surface-variant font-label-md text-label-md mb-1">잔여 패키지</p>
<p class="text-headline-md font-bold text-secondary">6</p>
</div>
<div class="bg-surface-container-high p-3 rounded-full">
<span class="material-symbols-outlined text-on-surface">inventory_2</span>
</div>
</div>
<div class="bg-surface-container-lowest p-stack-lg rounded-xl border border-outline-variant flex items-center justify-between">
<div>
<p class="text-on-surface-variant font-label-md text-label-md mb-1">이행물 진행</p>
<p class="text-headline-md font-bold text-tertiary-container">72%</p>
</div>
<div class="bg-tertiary-fixed p-3 rounded-full">
<span class="material-symbols-outlined text-on-tertiary-fixed">task_alt</span>
</div>
</div>
</section>
<!-- Main Dashboard Layout -->
<div class="grid grid-cols-3 gap-stack-lg">
<!-- Left 2/3: Sponsorship Tiers -->
<section class="col-span-2 space-y-6">
<div class="flex items-center justify-between">
<h2 class="font-headline-md text-headline-md text-on-surface">Sponsorship Tiers</h2>
<button class="text-primary font-bold text-label-md hover:underline">패키지 설정 관리</button>
</div>
<div class="grid grid-cols-3 gap-6">
<!-- Diamond Tier -->
<div class="tier-card bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col border-l-4 border-l-[#FFD700]">
<div class="p-stack-lg flex-grow">
<div class="flex justify-between items-start mb-4">
<span class="bg-[#FFD700]/10 text-[#B8860B] px-3 py-1 rounded-full text-label-md font-bold uppercase tracking-wider">Diamond</span>
<span class="bg-error text-on-error px-2 py-0.5 rounded text-label-xs font-bold">D-12</span>
</div>
<h3 class="text-title-sm font-bold mb-1">다이아몬드</h3>
<p class="text-headline-md font-extrabold text-primary mb-6">₩50,000,000</p>
<ul class="space-y-4 mb-8">
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
메인 로고 노출 (온/오프라인)
</li>
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
전시 부스 100sqm (최우선 배정)
</li>
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
연사 세션 2회 기회 부여
</li>
</ul>
</div>
<div class="p-stack-lg bg-surface-container-low border-t border-outline-variant flex justify-between items-center">
<p class="text-label-md font-bold text-on-surface-variant">잔여 <span class="text-primary">1</span></p>
<button class="bg-primary text-on-primary px-4 py-2 rounded font-bold text-label-md hover:opacity-90">선택</button>
</div>
</div>
<!-- Gold Tier -->
<div class="tier-card bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col border-l-4 border-l-outline-variant opacity-80 grayscale-[0.5]">
<div class="p-stack-lg flex-grow">
<div class="flex justify-between items-start mb-4">
<span class="bg-outline-variant/20 text-on-surface-variant px-3 py-1 rounded-full text-label-md font-bold uppercase tracking-wider">Gold</span>
</div>
<h3 class="text-title-sm font-bold mb-1">골드</h3>
<p class="text-headline-md font-extrabold text-primary mb-6">₩30,000,000</p>
<ul class="space-y-4 mb-8">
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
서브 로고 노출
</li>
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
전시 부스 50sqm
</li>
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
연사 세션 1회
</li>
</ul>
</div>
<div class="p-stack-lg bg-surface-container-low border-t border-outline-variant flex justify-between items-center">
<p class="text-label-md font-bold text-error">판매 완료</p>
<button class="bg-outline text-on-surface-variant px-4 py-2 rounded font-bold text-label-md cursor-not-allowed" disabled="">종료</button>
</div>
</div>
<!-- Silver Tier -->
<div class="tier-card bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col border-l-4 border-l-[#CD7F32]">
<div class="p-stack-lg flex-grow">
<div class="flex justify-between items-start mb-4">
<span class="bg-[#CD7F32]/10 text-[#8B4513] px-3 py-1 rounded-full text-label-md font-bold uppercase tracking-wider">Silver</span>
</div>
<h3 class="text-title-sm font-bold mb-1">실버</h3>
<p class="text-headline-md font-extrabold text-primary mb-6">₩15,000,000</p>
<ul class="space-y-4 mb-8">
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
일반 로고 노출
</li>
<li class="flex items-start gap-3 text-body-sm text-on-surface-variant">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
전시 부스 20sqm
</li>
</ul>
</div>
<div class="p-stack-lg bg-surface-container-low border-t border-outline-variant flex justify-between items-center">
<p class="text-label-md font-bold text-on-surface-variant">잔여 <span class="text-primary">3</span></p>
<button class="bg-primary text-on-primary px-4 py-2 rounded font-bold text-label-md hover:opacity-90">선택</button>
</div>
</div>
</div>
<!-- Additional Content Block for Context -->
<div class="bg-surface-container-high rounded-xl p-8 flex items-center justify-between border border-outline-variant">
<div class="flex items-center gap-6">
<div class="bg-white p-4 rounded-lg shadow-sm">
<img alt="Calendar" class="w-12 h-12" src="https://lh3.googleusercontent.com/aida/AP1WRLvpb_qiARCcrTrv6YnOIVjyk5szWwIfCGaaCmyRAnuu9aficeKfPTRrRHDgl4gJCHQUc7LYwtF1Ak04oSwBAezPs20C51ZjzHDaBCkRYJaYqHS-DOPHSGLdeLL_S2tlRlUAG-XF_ib2I1Svtqq26yY0uxmEQA5cWHJabFRv_0WVtBKiSP2QbCssxeccpcVHcWr_6CJ6zXonlepkxJcDXURlfHpvg7P2-O1urNto3A0xsepsB1ucua9Vnw"/>
</div>
<div>
<h4 class="font-title-sm text-title-sm text-on-surface">스폰서십 체결 일정</h4>
<p class="text-body-sm text-on-surface-variant">최종 마감일까지 14일 남았습니다. 주요 기업 팔로업이 필요합니다.</p>
</div>
</div>
<button class="text-primary font-bold px-6 py-3 rounded border-2 border-primary hover:bg-primary/5 transition-all">전체 일정 보기</button>
</div>
</section>
<!-- Right 1/3: Sponsor Management Panel -->
<section class="space-y-6">
<div class="flex items-center justify-between">
<h2 class="font-headline-md text-headline-md text-on-surface">스폰서 관리</h2>
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer">filter_list</span>
</div>
<!-- Active Sponsors List -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col shadow-sm">
<div class="p-4 border-b border-outline-variant bg-surface-container-low">
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]">search</span>
<input class="w-full bg-white border border-outline-variant rounded-full py-2 pl-10 pr-4 text-body-sm focus:ring-primary focus:border-primary" placeholder="스폰서 검색..." type="text"/>
</div>
</div>
<div class="max-h-[300px] overflow-y-auto custom-scrollbar">
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low cursor-pointer border-l-4 border-primary bg-primary-container/5 transition-colors">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded bg-primary-fixed flex items-center justify-center text-primary font-bold">T</div>
<div>
<p class="font-bold text-body-base">(주)테크솔루션</p>
<p class="text-label-xs text-on-surface-variant">다이아몬드 패키지</p>
</div>
</div>
<span class="bg-tertiary-fixed text-on-tertiary-fixed-variant px-2 py-0.5 rounded text-label-xs font-bold">계약완료</span>
</div>
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low cursor-pointer border-b border-outline-variant/30 transition-colors">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded bg-secondary-fixed flex items-center justify-center text-secondary font-bold">G</div>
<div>
<p class="font-bold text-body-base">글로벌바이오</p>
<p class="text-label-xs text-on-surface-variant">골드 패키지</p>
</div>
</div>
<span class="bg-surface-container-high text-on-surface-variant px-2 py-0.5 rounded text-label-xs font-bold">결제대기</span>
</div>
<div class="p-4 flex items-center justify-between hover:bg-surface-container-low cursor-pointer border-b border-outline-variant/30 transition-colors">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded bg-surface-dim flex items-center justify-center text-on-surface font-bold">N</div>
<div>
<p class="font-bold text-body-base">넥스트 인더스트리</p>
<p class="text-label-xs text-on-surface-variant">실버 패키지</p>
</div>
</div>
<span class="bg-tertiary-fixed text-on-tertiary-fixed-variant px-2 py-0.5 rounded text-label-xs font-bold">계약완료</span>
</div>
</div>
<!-- Fulfillment Checklist -->
<div class="p-stack-lg bg-surface-container-low border-t border-outline-variant">
<h4 class="font-label-md text-label-md text-on-surface uppercase tracking-tight mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-[18px]">fact_check</span>
(주)테크솔루션 이행 현황
</h4>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer group">
<input checked="" class="rounded border-outline-variant text-primary focus:ring-primary h-5 w-5" type="checkbox"/>
<span class="text-body-sm text-on-surface group-hover:text-primary">공식 홈페이지 로고 노출</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input checked="" class="rounded border-outline-variant text-primary focus:ring-primary h-5 w-5" type="checkbox"/>
<span class="text-body-sm text-on-surface group-hover:text-primary">전시 홀 부스 배정 (A-101)</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="rounded border-outline-variant text-primary focus:ring-primary h-5 w-5" type="checkbox"/>
<span class="text-body-sm text-on-surface group-hover:text-primary">연사 세션 주제 선정</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="rounded border-outline-variant text-primary focus:ring-primary h-5 w-5" type="checkbox"/>
<span class="text-body-sm text-on-surface group-hover:text-primary">브로슈어 광고 인쇄</span>
</label>
</div>
<a class="mt-8 flex items-center justify-between bg-primary-container/20 p-4 rounded-lg group hover:bg-primary-container/30 transition-all" href="#">
<div class="flex items-center gap-3">
<img alt="Dashboard" class="w-8 h-8" src="https://lh3.googleusercontent.com/aida/AP1WRLtO2EjzKAcW5wn4YYTv12rmtFAvwxlN4fmvGGktwdLqfBGzTTC8C_nxjBMV0s73Yz_eqq3my6ASAxnQk9MMCNSeoTVuQwK9CU8en_ZU63PoTk6muzScTpKFEU2aFgRqpZAs3IBLHhlZXl4ENmWt1GO_lvpxzlx-FAVMFPeSHBfqi8lMEy8xvdRkiRB0MpVPgcEnBWn0SU0BsxlrRVjlVMlL3gAwpYNlLruSqKP5tDnBgDNwjHSKU9SUV04"/>
<span class="font-bold text-label-md text-primary">노출·리드·ROI 대시보드</span>
</div>
<span class="material-symbols-outlined text-primary group-hover:translate-x-1 transition-transform">arrow_forward</span>
</a>
</div>
</div>
</section>
</div>
</div>
<!-- Footer for context -->
<footer class="mt-auto p-gutter text-center border-t border-outline-variant bg-surface-container-low">
<p class="text-label-xs text-on-surface-variant font-medium">© 2024 KINTEX Sponsorship Sales AI System. For internal administrative use only.</p>
</footer>
</main>
<!-- FAB for quick action (as per design rules) -->
<button class="fixed bottom-8 right-8 bg-secondary text-on-secondary w-14 h-14 rounded-full shadow-lg flex items-center justify-center hover:scale-110 transition-transform z-50">
<span class="material-symbols-outlined" style="font-variation-settings: 'wght' 600;">add</span>
</button>
</body></html>

View File

@ -1,395 +0,0 @@
<!DOCTYPE html>
<html lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX CMS - Content Publishing Workflow</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#003e28",
"secondary-fixed-dim": "#c9bfff",
"on-tertiary-fixed": "#002113",
"surface-container-lowest": "#ffffff",
"secondary": "#5427e6",
"on-primary-container": "#91c0ff",
"surface-container-highest": "#dbe2fa",
"surface-container-low": "#f1f3ff",
"inverse-on-surface": "#eef0ff",
"inverse-primary": "#a2c9ff",
"outline": "#727781",
"on-surface": "#141b2c",
"primary-fixed-dim": "#a2c9ff",
"error": "#ba1a1a",
"on-tertiary-fixed-variant": "#005236",
"surface-bright": "#faf8ff",
"surface-tint": "#24609e",
"on-error": "#ffffff",
"error-red": "#ba1a1a",
"inverse-surface": "#293042",
"tertiary-container": "#00583b",
"on-primary": "#ffffff",
"surface-dim": "#d3d9f1",
"primary": "#003765",
"surface-variant": "#dbe2fa",
"on-secondary-fixed-variant": "#4500d8",
"on-tertiary": "#ffffff",
"on-primary-fixed-variant": "#004881",
"secondary-fixed": "#e5deff",
"tertiary-fixed-dim": "#72daa9",
"primary-fixed": "#d3e4ff",
"on-secondary-container": "#f3edff",
"tertiary-fixed": "#8ef7c4",
"on-secondary-fixed": "#1b0063",
"on-primary-fixed": "#001c38",
"outline-variant": "#c1c7d3",
"surface-container-high": "#e1e7ff",
"on-background": "#141b2c",
"error-container": "#ffdad6",
"primary-container": "#004e8b",
"on-tertiary-container": "#68d1a0",
"secondary-container": "#6d4aff",
"surface": "#faf8ff",
"background": "#faf8ff",
"on-surface-variant": "#424750",
"on-secondary": "#ffffff",
"surface-container": "#eaedff",
"on-error-container": "#93000a"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "24px",
"container-padding": "24px",
"stack-sm": "8px",
"stack-md": "16px",
"section-padding": "96px",
"gutter": "16px",
"base": "4px"
},
"fontFamily": {
"display-lg": ["Inter"],
"label-xs": ["Geist"],
"headline-md": ["Inter"],
"body-base": ["Inter"],
"body-sm": ["Inter"],
"title-sm": ["Inter"],
"label-md": ["Geist"],
"display-lg-mobile": ["Inter"]
},
"fontSize": {
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', sans-serif; background-color: #faf8ff; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.glass-panel { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); }
.editor-container { height: calc(100vh - 64px); }
</style>
</head>
<body class="bg-surface-bright text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-container-padding h-16 sticky top-0 z-50 bg-surface-bright border-b border-outline-variant">
<div class="flex items-center gap-stack-md">
<span class="text-headline-md font-headline-md font-bold text-primary">KINTEX CMS</span>
<div class="hidden md:flex items-center ml-8 gap-stack-lg">
<nav class="flex gap-stack-md">
<a class="text-primary font-bold border-b-2 border-primary text-label-md font-label-md py-5" href="#">Content Hub</a>
<a class="text-on-surface-variant hover:bg-surface-container-low transition-colors text-label-md font-label-md py-5" href="#">Analytics</a>
<a class="text-on-surface-variant hover:bg-surface-container-low transition-colors text-label-md font-label-md py-5" href="#">Settings</a>
</nav>
</div>
</div>
<div class="flex items-center gap-stack-md">
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant">search</span>
<input class="pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm w-64 focus:ring-2 focus:ring-primary outline-none" placeholder="검색..." type="text"/>
</div>
<button class="p-2 text-on-surface-variant hover:bg-surface-container-low rounded-full transition-colors">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="bg-primary text-on-primary px-4 py-2 rounded-lg text-label-md font-label-md font-semibold hover:opacity-90 transition-opacity">
Save Draft
</button>
<div class="w-8 h-8 rounded-full bg-surface-container-high overflow-hidden border border-outline-variant">
<img class="w-full h-full object-cover" data-alt="A professional headshot of a business administrator in a sharp navy suit, lit with clean studio lighting against a soft gray background, representing a high-level CMS user profile." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAqk3I4MFteWFMBQwQB6FK8BzyB20dwTRHH5urizAdcerxyYtC3ah8-je_RMU5wpfo3trwbosEZhUtjqUWL2lV6U5MVODRBdSskZx72Shz4k0t70Fxb7aYKjHGADbQa44OYHTVVNg5orWl2rGRkWRzJSRogosSz7KsExmBapqivwlJcdbdDwVYCixyQjuopAXJA3ewsCQTVFo0yz0om8FTPXnQRw5fCtvekpuGmr-vkKpZeXHYE6mqQtHZa6G1Waqh1QhfXGilGqeM"/>
</div>
</div>
</header>
<div class="flex editor-container overflow-hidden">
<!-- SideNavBar -->
<aside class="fixed left-0 top-0 h-screen flex flex-col p-stack-md z-40 bg-surface-container-low border-r border-outline-variant w-64 pt-20">
<div class="mb-8 px-4">
<img alt="KINTEX Logo" class="h-8 w-auto mb-4" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAm5hp64QZLvzTJxp4zntrdm8Jn03ThGRPjUp1iVNB2yJg7ahN5sSzubjs9Qhsif7EkQr1oc5PWvb-vIAetO4b-Cp3b5CFFcMQMX1dIr3wMIYzkXPvZ8eKhDZ8-WldHZY0LyOLxv60Fe9Tx_GM_B5Q-Svj0NXUkOm-ScRyJuIUKDAQ312K7KEoUezQRakP-UxHVwD1Lv3va4MGdt6HSpGbBU6yPewnCnCtb49CG0KbqpNo8PBRpvtuaRN7xJ0sTh8LjadWP-rwiUlY"/>
<div class="text-title-sm font-title-sm font-extrabold text-on-surface">Global Admin</div>
<div class="text-body-sm font-body-sm text-on-surface-variant">Exhibition Management</div>
</div>
<nav class="flex flex-col gap-base flex-grow">
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg transition-all text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">dashboard</span> Dashboard
</a>
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg transition-all text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">event</span> Exhibitions
</a>
<a class="flex items-center gap-stack-sm bg-secondary-container text-on-secondary-container rounded-lg px-4 py-2 font-semibold text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">article</span> Content Hub
</a>
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg transition-all text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">perm_media</span> Media Library
</a>
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg transition-all text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">insights</span> Analytics
</a>
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg transition-all text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">admin_panel_settings</span> User Access
</a>
</nav>
<div class="mt-auto border-t border-outline-variant pt-4 flex flex-col gap-base">
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">contact_support</span> Support
</a>
<a class="flex items-center gap-stack-sm text-on-surface-variant hover:bg-surface-container-high px-4 py-2 rounded-lg text-label-md font-label-md" href="#">
<span class="material-symbols-outlined">memory</span> System Status
</a>
</div>
</aside>
<!-- Main CMS Workspace -->
<main class="ml-64 flex-grow grid grid-cols-12 gap-0 overflow-hidden">
<!-- Left Column: Content List -->
<section class="col-span-3 border-r border-outline-variant flex flex-col bg-surface-container-lowest">
<div class="p-stack-md border-b border-outline-variant">
<div class="flex gap-stack-sm mb-stack-md">
<button class="px-3 py-1 bg-primary-fixed text-on-primary-fixed text-label-md font-label-md rounded-full">한국어</button>
<button class="px-3 py-1 hover:bg-surface-container-low text-on-surface-variant text-label-md font-label-md rounded-full transition-colors">English</button>
<button class="px-3 py-1 hover:bg-surface-container-low text-on-surface-variant text-label-md font-label-md rounded-full transition-colors">日本語</button>
<button class="px-3 py-1 hover:bg-surface-container-low text-on-surface-variant text-label-md font-label-md rounded-full transition-colors">中文</button>
</div>
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant scale-75">search</span>
<input class="w-full pl-9 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-lg text-body-sm focus:ring-1 focus:ring-primary outline-none" placeholder="콘텐츠 검색" type="text"/>
</div>
</div>
<div class="flex-grow overflow-y-auto p-stack-sm">
<div class="flex flex-col gap-base">
<div class="flex items-center justify-between p-stack-sm hover:bg-surface-container-low rounded-lg group cursor-pointer transition-colors">
<div class="flex items-center gap-stack-sm">
<span class="material-symbols-outlined text-on-surface-variant text-[20px]">description</span>
<span class="text-body-sm font-body-sm">페이지</span>
</div>
<span class="px-2 py-0.5 bg-surface-variant text-on-surface-variant text-label-xs font-label-xs rounded">게시</span>
</div>
<div class="flex items-center justify-between p-stack-sm hover:bg-surface-container-low rounded-lg group cursor-pointer transition-colors">
<div class="flex items-center gap-stack-sm">
<span class="material-symbols-outlined text-on-surface-variant text-[20px]">campaign</span>
<span class="text-body-sm font-body-sm">공지사항</span>
</div>
<span class="px-2 py-0.5 bg-error-container text-error text-label-xs font-label-xs rounded">초안</span>
</div>
<div class="flex items-center justify-between p-stack-sm bg-primary-container text-on-primary-container rounded-lg cursor-pointer">
<div class="flex items-center gap-stack-sm">
<span class="material-symbols-outlined text-[20px]">info</span>
<span class="text-body-sm font-semibold">행사 소개</span>
</div>
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 text-label-xs font-label-xs rounded">검수 중</span>
</div>
<div class="flex items-center justify-between p-stack-sm hover:bg-surface-container-low rounded-lg group cursor-pointer transition-colors">
<div class="flex items-center gap-stack-sm">
<span class="material-symbols-outlined text-on-surface-variant text-[20px]">person</span>
<span class="text-body-sm font-body-sm">연사 정보</span>
</div>
<span class="px-2 py-0.5 bg-green-100 text-green-800 text-label-xs font-label-xs rounded">게시 완료</span>
</div>
<div class="flex items-center justify-between p-stack-sm hover:bg-surface-container-low rounded-lg group cursor-pointer transition-colors">
<div class="flex items-center gap-stack-sm">
<span class="material-symbols-outlined text-on-surface-variant text-[20px]">help</span>
<span class="text-body-sm font-body-sm">FAQ</span>
</div>
<span class="px-2 py-0.5 bg-green-100 text-green-800 text-label-xs font-label-xs rounded">게시 완료</span>
</div>
</div>
</div>
</section>
<!-- Center Column: Editor -->
<section class="col-span-6 flex flex-col bg-surface-bright overflow-y-auto">
<div class="max-w-3xl mx-auto w-full p-container-padding py-stack-lg">
<div class="mb-stack-lg">
<label class="block text-label-md font-label-md text-on-surface-variant mb-base">제목</label>
<input class="w-full text-display-lg font-display-lg bg-transparent border-b border-outline-variant focus:border-primary outline-none pb-2 placeholder-on-surface-variant" type="text" value="행사 소개"/>
</div>
<div class="flex items-center gap-stack-sm border border-outline-variant rounded-lg bg-surface-container-lowest p-2 mb-stack-md sticky top-0 z-10 shadow-sm">
<button class="p-1.5 hover:bg-surface-container-low rounded"><span class="material-symbols-outlined text-[20px]">format_bold</span></button>
<button class="p-1.5 hover:bg-surface-container-low rounded"><span class="material-symbols-outlined text-[20px]">format_italic</span></button>
<button class="p-1.5 hover:bg-surface-container-low rounded"><span class="material-symbols-outlined text-[20px]">link</span></button>
<div class="w-px h-4 bg-outline-variant mx-1"></div>
<button class="p-1.5 hover:bg-surface-container-low rounded"><span class="material-symbols-outlined text-[20px]">format_list_bulleted</span></button>
<button class="p-1.5 hover:bg-surface-container-low rounded"><span class="material-symbols-outlined text-[20px]">image</span></button>
<button class="p-1.5 hover:bg-surface-container-low rounded"><span class="material-symbols-outlined text-[20px]">smart_button</span></button>
</div>
<div class="mb-stack-lg">
<div class="flex items-center justify-between mb-stack-sm">
<span class="text-label-md font-label-md text-on-surface-variant">미디어 라이브러리</span>
<button class="text-primary text-label-md font-label-md hover:underline">전체보기</button>
</div>
<div class="flex gap-stack-sm overflow-x-auto pb-2 scrollbar-hide">
<div class="flex-shrink-0 w-32 h-24 rounded-lg overflow-hidden border border-outline-variant relative group">
<img class="w-full h-full object-cover" data-alt="A high-resolution photograph of the KINTEX smart factory exhibition floor, showing advanced robotic arms and vibrant neon blue lighting, capturing the scale and future-focused technological atmosphere of the venue." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDCU4HA3zilpSkwVvKDJ19qbfaGz63eMF_abLxBjHeT0tJ8kA9wt3B9DLiBgSZ46uDkdwprXM-xAoJt6skBpeIclO4CvRO4y-EK1u_gguCHek4kU_r93NzCMl1F57bKNuh-R_3_uzDAEDIjki_9uNc6r_Jl28Mq_BTLnSqAMTBxlFQl5g6FXzQhrPyr38kQtb9bX4NLgCU_0elTmPX0uFse66PQZwSz32bAQcAd_PYtivw5CItRLLXyGDu0xsT6cfPCg7YU79XC5kM"/>
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<span class="material-symbols-outlined text-white">add_circle</span>
</div>
</div>
<div class="flex-shrink-0 w-32 h-24 rounded-lg overflow-hidden border border-outline-variant relative group">
<img class="w-full h-full object-cover" data-alt="A wide-angle architectural shot of the KINTEX exhibition hall, featuring a massive glass ceiling that floods the space with natural light, showcasing a modern corporate event with many attendees." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAu_4ZC-0o6tCp_6A8n3QZpbq-NBw73DtOnzX6ldE3HsvZ3p1nYzdjHRWH_AIn92bx9N8DDMCo9yBY4246krEKIneQDJQMcNdrxKuq4WFv4HGozcEOhMScLutgSMHD5LUVtT-VqlgjDEB6hln9RavNJeeD7nsYQoXlA52kPPb_mgslLMkhUUvLxvUHLv93G5sTuYtf9odxq6TiAGAa9qygiJXj9QY8WWl8hMG-M8kDT7sv9pY22AM3FdOLXhgbyVa77ePhyJeRhDzs"/>
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<span class="material-symbols-outlined text-white">add_circle</span>
</div>
</div>
<div class="flex-shrink-0 w-32 h-24 rounded-lg bg-surface-container-low flex items-center justify-center border border-dashed border-outline-variant hover:bg-surface-container-high transition-colors cursor-pointer">
<span class="material-symbols-outlined text-on-surface-variant">add_photo_alternate</span>
</div>
</div>
</div>
<!-- Content Blocks -->
<div class="space-y-stack-md">
<!-- Block 1: Image -->
<div class="relative rounded-xl overflow-hidden border border-outline-variant group">
<img class="w-full h-64 object-cover" data-alt="A wide cinematic shot of a modern exhibition hall with futuristic glowing elements and blue neon highlights, emphasizing a professional corporate atmosphere at KINTEX." src="https://lh3.googleusercontent.com/aida-public/AB6AXuD130FCzvP0yZkR2SylRLH9ZSZvyCzr1q1a-chKBXmUqqSu5JeUiSd0DR0gK_onTYep5hKt4iTosB8m8eY9C2FQV8qfwUbrnlukxRDx94YiHTpH632-9UDAinblMfheeDRzvZbtFYIvdAoUACltpRgtOhwzLkqf0axUlOc5MLfLYZVuBfz6fX811LbcTWb3CPtcFkx1oSNfQqjOzRIYVqSIWmr2dIvr7dlCxU81vG0JKWVb6Yb7CquG6Z9mY6J1JY8dLV-0-a1yUMA"/>
<div class="absolute top-2 right-2 flex gap-base opacity-0 group-hover:opacity-100 transition-opacity">
<button class="p-2 bg-surface rounded-full shadow-md hover:bg-surface-container-high"><span class="material-symbols-outlined text-[18px]">drag_indicator</span></button>
<button class="p-2 bg-surface rounded-full shadow-md text-error hover:bg-error-container"><span class="material-symbols-outlined text-[18px]">delete</span></button>
</div>
</div>
<!-- Block 2: Text -->
<div class="p-stack-md bg-surface-container-lowest border border-outline-variant rounded-xl group relative">
<p class="text-body-base font-body-base leading-relaxed">
KINTEX 스마트 팩토리 엑스포 2045는 글로벌 제조 혁신을 선도하는 최첨단 기술의 집약체입니다. 이번 전시에서는 AI 기반의 자동화 솔루션, 협동 로봇, 그리고 데이터 중심의 생산 최적화 기술을 한자리에서 만나보실 수 있습니다.
</p>
<div class="absolute -left-10 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="p-2 text-on-surface-variant hover:text-primary"><span class="material-symbols-outlined">drag_indicator</span></button>
</div>
</div>
<!-- Block 3: AI Summary Block -->
<div class="p-stack-md border-2 border-secondary/30 bg-secondary-fixed/10 rounded-xl relative overflow-hidden group">
<div class="flex items-center gap-stack-sm mb-stack-sm">
<span class="material-symbols-outlined text-secondary scale-75" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
<span class="text-label-md font-label-md text-secondary font-bold">AI 요약 정보</span>
</div>
<p class="text-body-sm font-body-sm text-on-surface italic">
"글로벌 제조 혁신의 중심, AI와 로봇 공학의 결합을 통해 미래 산업의 청사진을 제시하는 전시회입니다."
</p>
<div class="absolute -right-4 -bottom-4 opacity-10">
<span class="material-symbols-outlined text-[80px] text-secondary">auto_awesome</span>
</div>
</div>
</div>
<!-- Scheduling Section -->
<div class="mt-section-padding/4 pt-stack-lg border-t border-outline-variant">
<h3 class="text-title-sm font-title-sm mb-stack-sm">예약 게시 설정</h3>
<div class="flex items-center gap-stack-md p-stack-md bg-surface-container-low border border-outline-variant rounded-lg">
<div class="flex items-center gap-stack-sm text-primary">
<span class="material-symbols-outlined">calendar_today</span>
<span class="text-body-base font-semibold">2026.08.01</span>
</div>
<div class="w-px h-4 bg-outline-variant"></div>
<div class="flex items-center gap-stack-sm text-primary">
<span class="material-symbols-outlined">schedule</span>
<span class="text-body-base font-semibold">09:00</span>
</div>
<button class="ml-auto text-on-surface-variant hover:bg-surface-container-high p-2 rounded-full"><span class="material-symbols-outlined">edit</span></button>
</div>
</div></div></section>
<!-- Right Column: Publishing & Workflow -->
<section class="col-span-3 border-l border-outline-variant bg-surface-container-low flex flex-col p-container-padding">
<div class="mb-stack-lg">
<h2 class="text-title-sm font-title-sm text-on-surface mb-stack-md">게시 워크플로</h2>
<div class="flex flex-col gap-stack-sm">
<button class="w-full py-3 px-4 border border-outline text-on-surface hover:bg-surface-container-high rounded-lg text-label-md font-label-md font-semibold transition-colors">
검수 요청
</button>
<button class="w-full py-3 px-4 bg-primary text-on-primary hover:opacity-90 rounded-lg text-label-md font-label-md font-semibold transition-opacity flex items-center justify-center gap-stack-sm shadow-md">
<span class="material-symbols-outlined text-[18px]">publish</span> 최종 승인 및 게시
</button>
</div>
</div>
<div class="mb-stack-lg">
<div class="flex items-center justify-between mb-stack-sm">
<span class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">버전 히스토리</span>
<span class="material-symbols-outlined text-on-surface-variant scale-75">history</span>
</div>
<div class="space-y-stack-sm">
<div class="p-stack-sm bg-surface-container-lowest border-l-4 border-primary rounded-lg shadow-sm">
<div class="text-label-md font-bold text-primary">v3 (현재 편집 중)</div>
<div class="text-label-xs font-label-xs text-on-surface-variant">2026.07.25 · 관리자</div>
</div>
<div class="p-stack-sm bg-surface-container-low border-l-4 border-green-500 rounded-lg opacity-80">
<div class="text-label-md font-semibold text-on-surface">v2 (게시 중)</div>
<div class="text-label-xs font-label-xs text-on-surface-variant">2026.06.10 · 시스템</div>
</div>
<div class="p-stack-sm bg-surface-container-low border-l-4 border-outline-variant rounded-lg opacity-60">
<div class="text-label-md font-semibold text-on-surface">v1</div>
<div class="text-label-xs font-label-xs text-on-surface-variant">2026.05.15 · 관리자</div>
</div>
</div>
</div>
<div class="mb-stack-lg p-stack-md bg-primary/5 border border-primary/20 rounded-lg">
<div class="flex items-center gap-stack-sm mb-base">
<span class="material-symbols-outlined text-primary scale-75" style="font-variation-settings: 'FILL' 1;">event_available</span>
<span class="text-label-md font-bold text-primary">예약 정보</span>
</div>
<p class="text-body-sm font-body-sm text-on-surface">
2026.08.01 09:00 자동 게시 예약됨
</p>
</div>
<div class="mt-auto pt-stack-md border-t border-outline-variant">
<div class="flex items-center justify-between group cursor-pointer py-2">
<span class="text-label-md font-label-md text-on-surface">현장 사이니지 동시 배포</span>
<div class="w-10 h-6 bg-primary rounded-full relative p-1 transition-colors">
<div class="w-4 h-4 bg-white rounded-full absolute right-1 shadow-sm"></div>
</div>
</div>
<div class="flex items-center justify-between group cursor-pointer py-2">
<span class="text-label-md font-label-md text-on-surface">메일링 리스트 통보</span>
<div class="w-10 h-6 bg-outline-variant rounded-full relative p-1 transition-colors">
<div class="w-4 h-4 bg-white rounded-full absolute left-1 shadow-sm"></div>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- Micro-interaction Script -->
<script>
document.querySelectorAll('button, a').forEach(el => {
el.addEventListener('click', (e) => {
const ripple = document.createElement('div');
ripple.classList.add('absolute', 'bg-white/20', 'rounded-full', 'animate-ping');
ripple.style.width = '20px';
ripple.style.height = '20px';
ripple.style.left = `${e.clientX}px`;
ripple.style.top = `${e.clientY}px`;
// Add logic for button feedback if needed
});
});
</script>
</body></html>

View File

@ -1,437 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI System - 마이크로사이트 빌더</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-surface": "#141b2c",
"surface-container-high": "#e1e7ff",
"error-container": "#ffdad6",
"surface-container-low": "#f1f3ff",
"inverse-surface": "#293042",
"surface-container": "#eaedff",
"secondary-fixed-dim": "#c9bfff",
"outline": "#727781",
"background": "#faf8ff",
"surface-container-lowest": "#ffffff",
"inverse-primary": "#a2c9ff",
"on-background": "#141b2c",
"surface-tint": "#24609e",
"on-tertiary-fixed": "#002113",
"on-secondary": "#ffffff",
"primary-fixed": "#d3e4ff",
"primary-container": "#004e8b",
"surface-variant": "#dbe2fa",
"surface-bright": "#f9f9ff",
"secondary-fixed": "#e5deff",
"surface": "#faf8ff",
"primary-fixed-dim": "#a2c9ff",
"inverse-on-surface": "#eef0ff",
"primary": "#003765",
"on-tertiary-container": "#68d1a0",
"on-secondary-fixed-variant": "#4500d8",
"on-secondary-container": "#f3edff",
"on-surface-variant": "#424750",
"on-secondary-fixed": "#1b0063",
"tertiary": "#003e28",
"secondary-container": "#6d4aff",
"error": "#ba1a1a",
"surface-dim": "#d3d9f1",
"surface-container-highest": "#dbe2fa",
"on-tertiary-fixed-variant": "#005236",
"on-error-container": "#93000a",
"outline-variant": "#c1c7d3",
"on-primary-fixed": "#001c38",
"on-primary": "#ffffff",
"tertiary-fixed": "#8ef7c4",
"on-primary-container": "#91c0ff",
"error-red": "#ba1a1a",
"on-tertiary": "#ffffff",
"tertiary-container": "#00583b",
"tertiary-fixed-dim": "#72daa9",
"on-primary-fixed-variant": "#004881",
"secondary": "#5427e6",
"on-error": "#ffffff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "24px",
"base": "4px",
"container-padding": "24px",
"gutter": "16px",
"stack-md": "16px",
"section-padding": "96px",
"stack-sm": "8px"
},
"fontFamily": {
"display-lg": ["Inter"],
"body-base": ["Inter"],
"title-sm": ["Inter"],
"headline-md": ["Inter"],
"label-md": ["Geist"]
},
"fontSize": {
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 10px;
}
.canvas-bg {
background-image: radial-gradient(#c1c7d3 0.5px, transparent 0.5px);
background-size: 20px 20px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
</style>
</head>
<body class="bg-background text-on-surface font-body-base antialiased overflow-hidden">
<!-- TopAppBar Shell -->
<header class="bg-surface-container-lowest border-b border-outline-variant shadow-sm fixed top-0 z-50 flex justify-between items-center w-full px-6 h-16">
<div class="flex items-center gap-8">
<h1 class="font-display-lg text-display-lg font-bold text-primary">KINTEX AI System</h1>
<nav class="hidden md:flex items-center gap-6">
<a class="text-on-surface-variant hover:text-primary transition-colors font-label-md text-label-md" href="#">대시보드</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-label-md text-label-md" href="#">참가업체 관리</a>
<a class="text-primary border-b-2 border-primary font-bold pb-1 font-label-md text-label-md" href="#">마이크로사이트 빌더</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-label-md text-label-md" href="#">데이터 분석</a>
</nav>
</div>
<div class="flex items-center gap-4">
<button class="px-4 py-2 border border-outline-variant rounded-full text-on-surface font-label-md text-label-md hover:bg-surface-container-low transition-all">미리보기</button>
<button class="px-4 py-2 bg-primary text-on-primary rounded-full font-label-md text-label-md shadow-sm hover:opacity-90 transition-all">배포하기</button>
<div class="flex items-center gap-2 ml-4">
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer p-2 hover:bg-surface-container rounded-full">notifications</span>
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer p-2 hover:bg-surface-container rounded-full">account_circle</span>
</div>
</div>
</header>
<div class="flex h-screen pt-16">
<!-- Left Panel: Section Editor -->
<aside class="w-72 bg-surface-container-low border-r border-outline-variant flex flex-col h-full overflow-y-auto custom-scrollbar">
<div class="p-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 bg-primary-container rounded-xl flex items-center justify-center">
<span class="material-symbols-outlined text-on-primary-container">auto_awesome</span>
</div>
<div>
<h2 class="font-title-sm text-title-sm text-primary">사이트 빌더</h2>
<p class="text-xs text-on-surface-variant">(주)한빛로보틱스</p>
</div>
</div>
<div class="space-y-2 mb-8">
<div class="text-xs font-bold text-outline mb-3 uppercase tracking-wider">섹션 편집</div>
<!-- Section Items -->
<div class="bg-primary-container text-on-primary-container rounded-xl p-3 flex items-center justify-between group cursor-pointer shadow-sm">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-sm">drag_indicator</span>
<span class="font-label-md text-label-md">부스 소개</span>
</div>
<div class="flex items-center gap-1">
<span class="material-symbols-outlined text-sm">visibility</span>
<span class="material-symbols-outlined text-sm">edit</span>
</div>
</div>
<div class="text-on-surface-variant hover:bg-surface-container-high rounded-xl p-3 flex items-center justify-between group cursor-pointer transition-all">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-sm">drag_indicator</span>
<span class="font-label-md text-label-md">제품</span>
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<span class="material-symbols-outlined text-sm">visibility</span>
<span class="material-symbols-outlined text-sm">edit</span>
</div>
</div>
<div class="text-on-surface-variant hover:bg-surface-container-high rounded-xl p-3 flex items-center justify-between group cursor-pointer transition-all">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-sm">drag_indicator</span>
<span class="font-label-md text-label-md">예상샷 갤러리</span>
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<span class="material-symbols-outlined text-sm">visibility</span>
<span class="material-symbols-outlined text-sm">edit</span>
</div>
</div>
<div class="text-on-surface-variant hover:bg-surface-container-high rounded-xl p-3 flex items-center justify-between group cursor-pointer transition-all">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-sm">drag_indicator</span>
<span class="font-label-md text-label-md">연락처</span>
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<span class="material-symbols-outlined text-sm">visibility</span>
<span class="material-symbols-outlined text-sm">edit</span>
</div>
</div>
<button class="w-full mt-4 flex items-center justify-center gap-2 py-3 border-2 border-dashed border-outline-variant rounded-xl text-on-surface-variant hover:bg-surface-container-high transition-all text-sm font-medium">
<span class="material-symbols-outlined text-sm">add</span> 섹션 추가
</button>
</div>
<div class="space-y-4">
<div class="text-xs font-bold text-outline mb-1 uppercase tracking-wider">테마 설정</div>
<div>
<p class="text-xs text-on-surface-variant mb-2">기본 색상</p>
<div class="flex gap-3">
<div class="w-8 h-8 rounded-full bg-primary border-2 border-white ring-2 ring-primary ring-offset-1 cursor-pointer"></div>
<div class="w-8 h-8 rounded-full bg-secondary cursor-pointer hover:scale-110 transition-transform"></div>
<div class="w-8 h-8 rounded-full bg-[#10B981] cursor-pointer hover:scale-110 transition-transform"></div>
<div class="w-8 h-8 rounded-full bg-[#EF4444] cursor-pointer hover:scale-110 transition-transform"></div>
</div>
</div>
<div>
<p class="text-xs text-on-surface-variant mb-2">폰트 패밀리</p>
<div class="relative">
<select class="w-full appearance-none bg-surface-container-lowest border border-outline-variant rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-primary outline-none">
<option>Inter (Corporate)</option>
<option>Geist (Modern Mono)</option>
<option>Noto Sans KR</option>
</select>
<span class="material-symbols-outlined absolute right-2 top-2 text-sm pointer-events-none">expand_more</span>
</div>
</div>
</div>
</div>
<div class="mt-auto p-6 border-t border-outline-variant">
<div class="flex gap-4">
<button class="flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary flex-1">
<span class="material-symbols-outlined">settings</span>
<span class="text-[10px] font-bold">설정</span>
</button>
<button class="flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary flex-1">
<span class="material-symbols-outlined">help</span>
<span class="text-[10px] font-bold">도움말</span>
</button>
</div>
</div>
</aside>
<!-- Center: Canvas -->
<main class="flex-1 bg-surface-container-low canvas-bg overflow-y-auto p-12 custom-scrollbar">
<div class="max-w-4xl mx-auto flex flex-col gap-6">
<!-- Canvas Toolbar -->
<div class="flex justify-center">
<div class="bg-surface-container-lowest shadow-sm rounded-full p-1 flex items-center border border-outline-variant">
<button class="px-6 py-2 bg-primary text-on-primary rounded-full flex items-center gap-2 text-sm font-medium transition-all">
<span class="material-symbols-outlined text-sm">desktop_windows</span> 데스크톱
</button>
<button class="px-6 py-2 text-on-surface-variant hover:bg-surface-container-low rounded-full flex items-center gap-2 text-sm font-medium transition-all">
<span class="material-symbols-outlined text-sm">smartphone</span> 모바일
</button>
</div>
</div>
<!-- Preview Container -->
<div class="bg-white rounded-2xl shadow-xl overflow-hidden min-h-[1200px] border border-outline-variant">
<!-- Microsite Header -->
<div class="h-16 flex items-center justify-between px-8 border-b border-surface-container">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center text-white font-bold italic">H</div>
<span class="font-bold text-primary">(주)한빛로보틱스</span>
</div>
<div class="flex gap-6 text-sm font-medium text-on-surface-variant">
<span>소개</span>
<span>제품</span>
<span>갤러리</span>
<span class="text-primary">문의하기</span>
</div>
</div>
<!-- Hero Banner Section -->
<section class="relative h-[480px] flex items-center overflow-hidden">
<div class="absolute inset-0 z-0">
<div class="w-full h-full bg-cover bg-center transition-transform duration-700 hover:scale-105" data-alt="A grand, hyper-realistic scale model or wide-angle shot of a futuristic robotics exhibition booth at KINTEX. The booth, belonging to Hanbit Robotics, features a sleek curved canopy with glowing cyan light strips, large high-definition screens displaying robotic arms in motion, and a clean white floor with minimalist product pedestals. The atmosphere is sophisticated and professional with a cool blue and silver color palette." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuA7uBjYLKpPaMKGi8aFWL_OEefz3AabfH0XIMUK6HNI31wfUJwDrCshSuIXlOsuJQMsyGSSiXLrDcajMJmELEe9uGZOjSvw71s-enc4OQh3wtZJCrHD9jT3PnB5Y5IAHJbCFmfHisw4qd7yp7SBU9kUXUn1CJcJp9umXfGoWRBf1diGgwyCnRdKx72leksFRVUmugrE0cFspap2-4F6y7_haPUG1R5B05sbBugseADBFI0mSMcU2UxAgssfbQwpq_a3cEO2wV2neqo')"></div>
<div class="absolute inset-0 bg-gradient-to-r from-primary/90 to-transparent"></div>
</div>
<div class="relative z-10 px-12 max-w-2xl text-white">
<div class="inline-block px-3 py-1 bg-white/10 backdrop-blur-md border border-white/20 rounded-full text-xs font-bold mb-4">KINTEX AI EXPO 2024</div>
<h1 class="text-5xl font-bold mb-6 leading-tight">지능형 로보틱스의<br/>새로운 지평을 열다</h1>
<p class="text-lg text-white/80 mb-8 font-light">산업용 로봇부터 서비스 자동화 솔루션까지, 한빛로보틱스가 제시하는 미래 모빌리티 생태계를 경험하세요.</p>
<div class="flex gap-4">
<button class="px-8 py-3 bg-white text-primary rounded-lg font-bold shadow-lg hover:bg-surface-container-low transition-all">미팅 예약하기</button>
<button class="px-8 py-3 border border-white/30 backdrop-blur-sm text-white rounded-lg font-bold hover:bg-white/10 transition-all">브로슈어 다운로드</button>
</div>
</div>
</section>
<!-- Product Grid Section -->
<section class="p-12 bg-surface-bright">
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-2">
<div class="w-1 h-6 bg-primary rounded-full"></div>
<h2 class="text-2xl font-bold text-on-surface">주요 혁신 제품</h2>
</div>
<button class="text-primary font-bold text-sm flex items-center">전체 보기 <span class="material-symbols-outlined text-sm">chevron_right</span></button>
</div>
<div class="grid grid-cols-2 gap-8">
<!-- Card 1 -->
<div class="group bg-white rounded-2xl border border-outline-variant overflow-hidden hover:shadow-lg transition-all duration-300">
<div class="h-48 relative overflow-hidden">
<div class="w-full h-full bg-cover bg-center group-hover:scale-110 transition-transform duration-500" data-alt="Close-up of a sophisticated industrial robotic arm painted in matte white with carbon fiber accents, performing precision assembly in a clean, brightly lit manufacturing environment. The lighting is crisp and cool, highlighting the metallic textures and fluid mechanical joints of the robot. The overall aesthetic is ultra-modern and corporate." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAJslcK6oCfsi8JeVmhJD8pAD8VJsDuDNGXgEe9hx-3nCmIygyucB3eto1yTsV12uMpQaH4XbNJ4s5JQIXyP_EHbq_iQVxFfqOQi-9HiK9NGumm7-nhecJtMYSbQiTr297BvruBRVtiaR9I_fcHXpVH7Vcx2fJpUP7kE2zgvyZAugX0eUMoodMCVuIZwrID9nHtFhzvQyJ30IOh6PJiHdT6GxMbQx_CUV0y1JRyNnn6WbfF5kQGmV9LysXIuD04BjwGydCYC8lYBbo')"></div>
<div class="absolute top-4 right-4 px-3 py-1 bg-primary text-white text-xs font-bold rounded-full">NEW</div>
</div>
<div class="p-6 border-l-4 border-primary">
<h3 class="text-lg font-bold mb-2">HR-A1 협동로봇</h3>
<p class="text-sm text-on-surface-variant mb-4">고정밀 센서와 AI 비전 시스템이 탑재된 다목적 협동로봇으로 안전한 협업 환경을 제공합니다.</p>
<button class="w-full py-2 bg-surface-container-low text-primary font-bold rounded-lg group-hover:bg-primary group-hover:text-white transition-colors">상세 사양 보기</button>
</div>
</div>
<!-- Card 2 -->
<div class="group bg-white rounded-2xl border border-outline-variant overflow-hidden hover:shadow-lg transition-all duration-300">
<div class="h-48 relative overflow-hidden">
<div class="w-full h-full bg-cover bg-center group-hover:scale-110 transition-transform duration-500" data-alt="A futuristic service robot with a friendly digital face interface, standing in a brightly lit corporate lounge area. The robot has a sleek minimalist white body with subtle blue LED glowing elements. In the background, blurred modern furniture and green plants enhance the high-end, futuristic hospitality vibe. Soft, natural light fills the space." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAs30mIWmJRFomTta5Difa0J0K_fzdDsPyn_WaHGcD2YQZ3RnZs9e0rhKQbB3TAKVddk42AtU6Asri1JxDiCCpKwKTbYpzqSVvSwXks7jg5sYL1UXhr8eKAjXxGG9PvyZIUFfDlwAhLITV6QKE9eZZmM_Gpiu8eI49ICVFCqKUdeHH9FG3DOg7wNmUstZBSebWdndX-4IRqOBYUT9IFKFwQu1cWnsNnejyng9RKk9r_6SQU8cCcyaYDbMrCvV7eI11L8X0kpCfn4Z4')"></div>
<div class="absolute top-4 right-4 px-3 py-1 bg-secondary text-white text-xs font-bold rounded-full">POPULAR</div>
</div>
<div class="p-6 border-l-4 border-secondary">
<h3 class="text-lg font-bold mb-2">ServiBot Elite</h3>
<p class="text-sm text-on-surface-variant mb-4">전시장 및 호텔용 지능형 서비스 로봇으로 다국어 응대 및 자율 주행 안내가 가능합니다.</p>
<button class="w-full py-2 bg-surface-container-low text-secondary font-bold rounded-lg group-hover:bg-secondary group-hover:text-white transition-colors">상세 사양 보기</button>
</div>
</div>
</div>
</section>
<!-- AI Gallery Section -->
<section class="p-12 bg-white">
<div class="flex items-center gap-2 mb-8">
<div class="w-1 h-6 bg-tertiary-container rounded-full"></div>
<h2 class="text-2xl font-bold text-on-surface">AI 부스 예상 시뮬레이션</h2>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="relative group rounded-xl overflow-hidden aspect-[4/3] border border-outline-variant">
<img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" src="https://lh3.googleusercontent.com/aida/AP1WRLv3H3Wpe0zBHSuxQpTcXDcpOdolguqYeCc-O6Er1f1v7kIW1OvM8jO7ML3Duw169HILHRn6lMMYiX7cmJr_Ym05mrjgXagaQQtoAwQ3TIC3ZyZ7Bg-e42UBTkKt_wIydOFlskhmY6h5XTNmXwnDqY17JnXhXpY9NLdpPzY080hy03Mi5oNj1b3jZGV5WGcflIMxg6M49tHGG6P0pJY7mBYNrf9Lbn9jeoGeFbtHqCltSbrfa0LqRPTzk68"/>
<div class="absolute bottom-2 right-2 text-[10px] text-white/50 bg-black/20 px-2 rounded backdrop-blur-sm">AI 생성 예상 이미지</div>
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<span class="material-symbols-outlined text-white text-3xl">zoom_in</span>
</div>
</div>
<div class="relative group rounded-xl overflow-hidden aspect-[4/3] border border-outline-variant">
<img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" src="https://lh3.googleusercontent.com/aida/AP1WRLvwgkiVy1bWjfMXTV1VcvcnbYRvB-US3OXaAPSL0Ygxv_wQgZIrq2LFFc6zFDQ4FBQEcagABYU-UNLHF3hZdikrom4ENpN563lI3gXSJgCvxknOhpcHhpO7dUktv5Wg-hEaNzVHNfG2_CxgXFtXxyg4BWlNciTRF_lzeYqEBDJuhUSOCmY2HG2o3e7DWbX4BnpnvDEzVX9pXL-Go3w2mFFA257GsBvLwm_DXFRNjSm_m0JNeKL_qCXcPog"/>
<div class="absolute bottom-2 right-2 text-[10px] text-white/50 bg-black/20 px-2 rounded backdrop-blur-sm">AI 생성 예상 이미지</div>
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<span class="material-symbols-outlined text-white text-3xl">zoom_in</span>
</div>
</div>
</div>
</section>
</div>
</div>
</main>
<!-- Right Panel: Publishing & SEO -->
<aside class="w-72 bg-surface-container-lowest border-l border-outline-variant flex flex-col h-full overflow-y-auto custom-scrollbar">
<div class="p-6 space-y-8">
<!-- Status & URL -->
<section>
<div class="flex items-center justify-between mb-4">
<span class="text-xs font-bold text-outline uppercase tracking-wider">사이트 상태</span>
<div class="flex items-center gap-1.5 px-2.5 py-1 bg-on-tertiary-container/10 text-on-tertiary-fixed-variant rounded-full text-[10px] font-bold">
<span class="w-1.5 h-1.5 bg-[#10B981] rounded-full animate-pulse"></span> 게시됨
</div>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold text-on-surface-variant">도메인 URL</label>
<div class="flex items-center gap-2 bg-surface-container-low p-2 rounded-lg border border-outline-variant">
<span class="text-xs text-primary truncate flex-1 font-medium">hanbit.expo.kintex.kr</span>
<button class="material-symbols-outlined text-sm hover:text-primary transition-colors">content_copy</button>
</div>
</div>
</section>
<!-- SEO Settings -->
<section class="space-y-4">
<span class="text-xs font-bold text-outline uppercase tracking-wider block">SEO 및 검색 엔진</span>
<div class="space-y-1">
<label class="text-[11px] font-bold text-on-surface-variant">페이지 타이틀</label>
<input class="w-full bg-white border border-outline-variant rounded-lg px-3 py-2 text-sm focus:ring-1 focus:ring-primary focus:border-primary outline-none" type="text" value="한빛로보틱스 | KINTEX AI 전시"/>
</div>
<div class="space-y-1">
<label class="text-[11px] font-bold text-on-surface-variant">메타 설명</label>
<textarea class="w-full bg-white border border-outline-variant rounded-lg px-3 py-2 text-sm h-24 resize-none focus:ring-1 focus:ring-primary focus:border-primary outline-none">KINTEX AI EXPO 2024 (주)한빛로보틱스 공식 마이크로사이트입니다. 최첨단 협동로봇과 서비스 로봇 솔루션을 만나보세요.</textarea>
</div>
</section>
<!-- Language Selection -->
<section class="space-y-3">
<span class="text-xs font-bold text-outline uppercase tracking-wider block">언어 설정</span>
<div class="flex bg-surface-container-low rounded-lg p-1 border border-outline-variant">
<button class="flex-1 py-1.5 text-xs font-bold bg-white text-primary shadow-sm rounded-md transition-all"></button>
<button class="flex-1 py-1.5 text-xs font-medium text-on-surface-variant hover:text-primary transition-all"></button>
<button class="flex-1 py-1.5 text-xs font-medium text-on-surface-variant hover:text-primary transition-all"></button>
<button class="flex-1 py-1.5 text-xs font-medium text-on-surface-variant hover:text-primary transition-all"></button>
</div>
</section>
<!-- Activity Feed -->
<section class="pt-4 border-t border-outline-variant">
<div class="flex items-center gap-2 mb-4 text-primary">
<span class="material-symbols-outlined text-sm">history</span>
<span class="text-xs font-bold">최근 작업 이력</span>
</div>
<div class="space-y-4">
<div class="flex gap-3">
<div class="w-2 h-2 rounded-full bg-primary mt-1.5"></div>
<div>
<p class="text-xs font-bold text-on-surface">갤러리 이미지 업데이트</p>
<p class="text-[10px] text-outline">10분 전 · 관리자</p>
</div>
</div>
<div class="flex gap-3">
<div class="w-2 h-2 rounded-full bg-outline-variant mt-1.5"></div>
<div>
<p class="text-xs text-on-surface-variant">SEO 메타 데이터 수정</p>
<p class="text-[10px] text-outline">2시간 전 · 관리자</p>
</div>
</div>
</div>
</section>
</div>
<!-- Footer Action -->
<div class="mt-auto p-6 bg-surface-container-lowest border-t border-outline-variant">
<button class="w-full py-3 bg-primary text-on-primary rounded-xl font-bold text-sm shadow-md hover:scale-[0.98] transition-transform">
전시 라이브 적용
</button>
</div>
</aside>
</div>
<!-- Floating Action for Canvas Info -->
<div class="fixed bottom-8 left-1/2 -translate-x-1/2 glass-effect border border-outline-variant/30 px-4 py-2 rounded-full shadow-lg flex items-center gap-4 z-40">
<div class="flex -space-x-2">
<div class="w-6 h-6 rounded-full border-2 border-white bg-blue-500"></div>
<div class="w-6 h-6 rounded-full border-2 border-white bg-purple-500"></div>
<div class="w-6 h-6 rounded-full border-2 border-white bg-gray-200 flex items-center justify-center text-[10px] font-bold">+2</div>
</div>
<div class="h-4 w-[1px] bg-outline-variant"></div>
<p class="text-xs font-medium text-on-surface">현재 3명의 사용자가 이 프로젝트를 수정 중입니다.</p>
</div>
<script>
// Simple micro-interaction for active states
document.querySelectorAll('aside nav a, aside .space-y-2 div').forEach(item => {
item.addEventListener('click', function() {
// This would handle real switching in a full app
console.log('Switching context to:', this.innerText.trim());
});
});
</script>
</body></html>

View File

@ -1,414 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - SCR-37 다국어 콘텐츠 관리</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-error": "#ffffff",
"on-secondary-fixed": "#1b0063",
"on-background": "#141b2c",
"surface-container-high": "#e1e7ff",
"on-tertiary": "#ffffff",
"tertiary": "#003e28",
"primary-fixed": "#d3e4ff",
"surface-dim": "#d3d9f1",
"secondary": "#5427e6",
"on-surface-variant": "#424750",
"tertiary-fixed-dim": "#72daa9",
"surface-container-highest": "#dbe2fa",
"inverse-on-surface": "#eef0ff",
"tertiary-fixed": "#8ef7c4",
"background": "#faf8ff",
"surface-container-low": "#f2f3ff",
"on-tertiary-fixed-variant": "#005236",
"surface-variant": "#dbe2fa",
"on-tertiary-container": "#68d1a0",
"surface-tint": "#24609e",
"surface-container-lowest": "#ffffff",
"secondary-container": "#6d4aff",
"on-secondary": "#ffffff",
"primary": "#003765",
"on-error-container": "#93000a",
"surface-container": "#eaedff",
"surface": "#faf8ff",
"on-primary-fixed-variant": "#004881",
"primary-container": "#004e8b",
"on-primary-fixed": "#001c38",
"inverse-surface": "#293042",
"on-primary": "#ffffff",
"on-tertiary-fixed": "#002113",
"secondary-fixed-dim": "#c9bfff",
"outline-variant": "#c1c7d3",
"surface-bright": "#faf8ff",
"on-secondary-container": "#f3edff",
"primary-fixed-dim": "#a2c9ff",
"secondary-fixed": "#e5deff",
"tertiary-container": "#00583b",
"on-surface": "#141b2c",
"error-red": "#ba1a1a",
"on-primary-container": "#91c0ff",
"error": "#ba1a1a",
"inverse-primary": "#a2c9ff",
"outline": "#727781",
"error-container": "#ffdad6",
"on-secondary-fixed-variant": "#4500d8"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"container-padding": "24px",
"gutter": "16px",
"base": "4px",
"stack-md": "16px",
"stack-lg": "24px",
"section-padding": "96px",
"stack-sm": "8px"
},
"fontFamily": {
"body-sm": ["Inter"],
"headline-md": ["Inter"],
"display-lg-mobile": ["Inter"],
"label-xs": ["Geist"],
"body-base": ["Inter"],
"title-sm": ["Inter"],
"label-md": ["Geist"],
"display-lg": ["Inter"]
},
"fontSize": {
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 10px;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.translation-row:hover {
background-color: #f2f3ff;
}
</style>
</head>
<body class="bg-surface-bright text-on-surface">
<!-- Side Navigation Shell -->
<nav class="bg-surface-container dark:bg-inverse-surface h-full w-64 fixed left-0 top-0 border-r border-outline-variant flex flex-col py-stack-lg px-gutter z-50">
<div class="mb-12">
<h1 class="font-display-lg text-display-lg font-black text-primary dark:text-primary-fixed">KINTEX AI</h1>
<p class="font-title-sm text-label-md text-on-surface-variant opacity-70">Exhibition Management</p>
</div>
<div class="space-y-2 flex-1">
<div class="text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-colors p-3 rounded-xl flex items-center gap-3 cursor-pointer">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
<span class="font-title-sm text-body-base">Dashboard</span>
</div>
<div class="bg-primary-container text-on-primary-container font-bold rounded-xl p-3 flex items-center gap-3 cursor-pointer">
<span class="material-symbols-outlined" data-icon="event">event</span>
<span class="font-title-sm text-body-base">Exhibitions</span>
</div>
<div class="text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-colors p-3 rounded-xl flex items-center gap-3 cursor-pointer">
<span class="material-symbols-outlined" data-icon="insights">insights</span>
<span class="font-title-sm text-body-base">AI Analytics</span>
</div>
<div class="text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-colors p-3 rounded-xl flex items-center gap-3 cursor-pointer">
<span class="material-symbols-outlined" data-icon="handshake">handshake</span>
<span class="font-title-sm text-body-base">Partners</span>
</div>
<div class="text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-colors p-3 rounded-xl flex items-center gap-3 cursor-pointer">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
<span class="font-title-sm text-body-base">Settings</span>
</div>
</div>
<div class="pt-6 border-t border-outline-variant space-y-2">
<div class="text-on-surface-variant hover:text-primary hover:bg-surface-container-high transition-colors p-3 rounded-xl flex items-center gap-3 cursor-pointer">
<span class="material-symbols-outlined" data-icon="help">help</span>
<span class="font-label-md">Help Center</span>
</div>
<button class="w-full bg-primary text-on-primary font-bold py-3 rounded-xl transition-all active:scale-95">
System Support
</button>
</div>
</nav>
<!-- Top AppBar Shell -->
<header class="fixed top-0 left-64 right-0 h-16 bg-surface-bright border-b border-outline-variant flex justify-between items-center px-container-padding z-40">
<div class="flex items-center gap-4">
<h2 class="font-headline-md text-headline-md font-bold text-primary">Account Management</h2>
<div class="flex gap-6 ml-8">
<a class="text-on-surface-variant hover:text-primary font-label-md" href="#">Profile</a>
<a class="text-primary border-b-2 border-primary pb-1 font-label-md" href="#">Security</a>
<a class="text-on-surface-variant hover:text-primary font-label-md" href="#">Preferences</a>
<a class="text-on-surface-variant hover:text-primary font-label-md" href="#">Billing</a>
</div>
</div>
<div class="flex items-center gap-4">
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]" data-icon="search">search</span>
<input class="pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm focus:ring-2 focus:ring-primary outline-none w-64" placeholder="Search resources..." type="text"/>
</div>
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-all opacity-80 hover:opacity-100" data-icon="notifications">notifications</span>
<img class="w-8 h-8 rounded-full border border-outline" data-alt="Professional studio portrait of a corporate administrator, high-end photography, clean blue lighting, office background blurred, ultra-detailed skin textures, professional attire, KINTEX executive profile photo style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBv7DMFbaqNrWXRwTQaT7B8H4Od5WlHgwSGgirOPt-zVPWdzIKbLCIf357IQMZG_XD3tWwcP9Yl-TFYUmH2wWN26Mx0w-cAfP3ZBRPQ_-QxZRwwyEv5fKddLYe2zJIaIUYlZ5zHnM6PPF6l6CYPwYK5RrCWVlCmU3rLqFmSRDzg-Jem8F6VEx1fAmtG7Uh8MpJrRLok4jwroiAtapD3n2LC0e2MbfJ5aspuXkYbIR_BPX95jr-cFoDQ5s6Q8uYSMddXny42w_hpSP4"/>
</div>
</header>
<!-- Main Content Canvas -->
<main class="ml-64 mt-16 p-stack-lg min-h-screen">
<!-- Summary Bar: Language Coverage -->
<div class="grid grid-cols-4 gap-gutter mb-stack-lg">
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-4 shadow-sm border-l-4 border-primary">
<div class="flex justify-between items-center mb-2">
<span class="text-label-md font-bold text-on-surface">한국어 (KO)</span>
<span class="text-label-md font-bold text-primary">100%</span>
</div>
<div class="w-full bg-surface-container h-2 rounded-full overflow-hidden">
<div class="bg-primary h-full w-[100%] transition-all duration-1000"></div>
</div>
</div>
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-4 shadow-sm border-l-4 border-secondary">
<div class="flex justify-between items-center mb-2">
<span class="text-label-md font-bold text-on-surface">English (EN)</span>
<span class="text-label-md font-bold text-secondary">82%</span>
</div>
<div class="w-full bg-surface-container h-2 rounded-full overflow-hidden">
<div class="bg-secondary h-full w-[82%] transition-all duration-1000 delay-100"></div>
</div>
</div>
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-4 shadow-sm border-l-4 border-tertiary">
<div class="flex justify-between items-center mb-2">
<span class="text-label-md font-bold text-on-surface">Chinese (ZH)</span>
<span class="text-label-md font-bold text-tertiary">60%</span>
</div>
<div class="w-full bg-surface-container h-2 rounded-full overflow-hidden">
<div class="bg-tertiary h-full w-[60%] transition-all duration-1000 delay-200"></div>
</div>
</div>
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-4 shadow-sm border-l-4 border-on-surface-variant">
<div class="flex justify-between items-center mb-2">
<span class="text-label-md font-bold text-on-surface">Japanese (JA)</span>
<span class="text-label-md font-bold text-on-surface-variant">45%</span>
</div>
<div class="w-full bg-surface-container h-2 rounded-full overflow-hidden">
<div class="bg-on-surface-variant h-full w-[45%] transition-all duration-1000 delay-300"></div>
</div>
</div>
</div>
<div class="flex gap-gutter h-[calc(100vh-280px)]">
<!-- Left: Translation Matrix -->
<div class="flex-1 bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col">
<!-- Toolbar -->
<div class="p-4 border-b border-outline-variant flex justify-between items-center">
<div class="flex gap-2 p-1 bg-surface-container rounded-lg">
<button class="px-4 py-1.5 rounded-md bg-white shadow-sm text-label-md font-bold text-primary">English (EN)</button>
<button class="px-4 py-1.5 rounded-md hover:bg-white/50 text-label-md font-medium text-on-surface-variant">Chinese (ZH)</button>
<button class="px-4 py-1.5 rounded-md hover:bg-white/50 text-label-md font-medium text-on-surface-variant">Japanese (JA)</button>
</div>
<button class="bg-secondary-container text-on-secondary px-4 py-2 rounded-full font-label-md flex items-center gap-2 hover:brightness-110 transition-all active:scale-95">
<span class="material-symbols-outlined text-[18px]" data-icon="auto_awesome" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
AI 자동 번역
</button>
</div>
<!-- Matrix Table -->
<div class="flex-1 overflow-auto custom-scrollbar">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-surface-container-low z-10">
<tr>
<th class="p-4 font-label-md text-on-surface-variant border-b border-outline-variant w-1/4">콘텐츠 ID / 원문 (KO)</th>
<th class="p-4 font-label-md text-on-surface-variant border-b border-outline-variant w-1/3">번역본 (Target)</th>
<th class="p-4 font-label-md text-on-surface-variant border-b border-outline-variant text-center">상태</th>
<th class="p-4 font-label-md text-on-surface-variant border-b border-outline-variant text-right">최근 업데이트</th>
</tr>
</thead>
<tbody class="text-body-sm text-on-surface divide-y divide-outline-variant">
<tr class="translation-row cursor-pointer transition-colors bg-primary-fixed/5">
<td class="p-4">
<div class="font-bold">행사명</div>
<div class="text-on-surface-variant text-[11px]">GLOBAL_EVENT_NAME</div>
</td>
<td class="p-4 italic">KINTEX Global Smart Tech Expo 2024</td>
<td class="p-4 text-center">
<span class="bg-on-tertiary-container/20 text-on-tertiary-fixed-variant px-3 py-1 rounded-full text-[11px] font-bold">검수완료</span>
</td>
<td class="p-4 text-right text-on-surface-variant">2시간 전</td>
</tr>
<tr class="translation-row cursor-pointer transition-colors">
<td class="p-4">
<div class="font-bold">홀 안내</div>
<div class="text-on-surface-variant text-[11px]">HALL_GUIDE</div>
</td>
<td class="p-4">Exhibition Hall Floor Information</td>
<td class="p-4 text-center">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-3 py-1 rounded-full text-[11px] font-bold">AI 초벌</span>
</td>
<td class="p-4 text-right text-on-surface-variant">1일 전</td>
</tr>
<tr class="translation-row cursor-pointer transition-colors">
<td class="p-4">
<div class="font-bold">참가 신청</div>
<div class="text-on-surface-variant text-[11px]">REGISTRATION</div>
</td>
<td class="p-4 text-on-surface-variant opacity-50">Pending translation...</td>
<td class="p-4 text-center">
<span class="bg-surface-container-high text-on-surface-variant px-3 py-1 rounded-full text-[11px] font-bold">미번역</span>
</td>
<td class="p-4 text-right text-on-surface-variant">-</td>
</tr>
<tr class="translation-row cursor-pointer transition-colors">
<td class="p-4">
<div class="font-bold">전시 개요</div>
<div class="text-on-surface-variant text-[11px]">EXHIBITION_OVERVIEW</div>
</td>
<td class="p-4">Overview of the upcoming tech showcase and participation guidelines.</td>
<td class="p-4 text-center">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-3 py-1 rounded-full text-[11px] font-bold">AI 초벌</span>
</td>
<td class="p-4 text-right text-on-surface-variant">3시간 전</td>
</tr>
<tr class="translation-row cursor-pointer transition-colors">
<td class="p-4">
<div class="font-bold">전시장 안내도</div>
<div class="text-on-surface-variant text-[11px]">FLOOR_MAP_ALT</div>
</td>
<td class="p-4">Detailed layout and booth mapping for KINTEX 1 and 2.</td>
<td class="p-4 text-center">
<span class="bg-on-tertiary-container/20 text-on-tertiary-fixed-variant px-3 py-1 rounded-full text-[11px] font-bold">검수완료</span>
</td>
<td class="p-4 text-right text-on-surface-variant">5일 전</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Right Side Panel: Editor -->
<div class="w-80 flex flex-col gap-gutter">
<!-- Editor Section -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl flex-1 flex flex-col overflow-hidden">
<div class="p-4 border-b border-outline-variant bg-surface-container-low flex justify-between items-center">
<span class="font-bold text-label-md">번역 편집: 원문(한) ↔ 번역(영)</span>
<span class="material-symbols-outlined text-[18px] text-on-surface-variant" data-icon="close">close</span>
</div>
<div class="p-4 space-y-4 flex-1 overflow-auto custom-scrollbar">
<div>
<label class="text-label-xs text-on-surface-variant uppercase mb-1 block">Selected ID</label>
<div class="text-body-sm font-bold text-primary">GLOBAL_EVENT_NAME</div>
</div>
<div>
<label class="text-label-xs text-on-surface-variant uppercase mb-1 block">원문 (KO)</label>
<div class="p-3 bg-surface-container-low rounded-lg border border-outline-variant text-body-sm font-medium">
행사명
</div>
</div>
<div>
<label class="text-label-xs text-on-surface-variant uppercase mb-1 block">번역 (EN)</label>
<textarea class="w-full h-24 p-3 bg-white rounded-lg border-2 border-secondary outline-none text-body-sm focus:ring-0 resize-none">KINTEX Global Smart Tech Expo 2024</textarea>
<div class="flex justify-end gap-2 mt-2">
<button class="text-secondary text-label-xs font-bold flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]" data-icon="sync" style="font-variation-settings: 'FILL' 1;">sync</span>
AI 재생성
</button>
</div>
</div>
<hr class="border-outline-variant"/>
<!-- Glossary Section -->
<div>
<div class="flex items-center gap-2 mb-2">
<span class="material-symbols-outlined text-[16px] text-primary" data-icon="book" style="font-variation-settings: 'FILL' 1;">book</span>
<span class="text-label-md font-bold">Glossary</span>
</div>
<div class="space-y-2">
<div class="flex justify-between items-center p-2 bg-surface-container-low rounded-md border border-outline-variant">
<span class="text-label-xs font-bold"></span>
<span class="text-label-xs text-on-surface-variant">→ Hall</span>
</div>
<div class="flex justify-between items-center p-2 bg-surface-container-low rounded-md border border-outline-variant">
<span class="text-label-xs font-bold">안내</span>
<span class="text-label-xs text-on-surface-variant">→ Guide</span>
</div>
</div>
</div>
<!-- Tone Guide Section -->
<div>
<div class="flex items-center gap-2 mb-2">
<span class="material-symbols-outlined text-[16px] text-secondary" data-icon="psychology" style="font-variation-settings: 'FILL' 1;">psychology</span>
<span class="text-label-md font-bold">Tone Guide</span>
</div>
<div class="p-3 bg-secondary-fixed/30 border border-secondary-container rounded-lg">
<div class="text-[11px] font-bold text-on-secondary-fixed-variant mb-1">비즈니스 격식체 (Formal Business)</div>
<p class="text-[11px] text-on-secondary-fixed leading-relaxed italic">
"AI 추천: 공식 웹사이트 제목에는 불필요한 관사를 생략하고 핵심 키워드 중심의 명사형 종결이 효과적입니다."
</p>
</div>
</div>
</div>
<div class="p-4 border-t border-outline-variant bg-surface-container-low grid grid-cols-2 gap-2">
<button class="py-2 border border-outline-variant bg-white rounded-lg font-bold text-label-md hover:bg-surface-container transition-all">취소</button>
<button class="py-2 bg-primary text-on-primary rounded-lg font-bold text-label-md hover:brightness-125 shadow-md transition-all active:scale-95">적용</button>
</div>
</div>
</div>
</div>
</main>
<!-- Footer Shell -->
<footer class="ml-64 bg-surface-container-low border-t border-outline-variant flex justify-between items-center py-4 px-container-padding mt-4">
<span class="font-label-xs text-on-surface-variant">© 2024 KINTEX AI Exhibition Management. All rights reserved.</span>
<div class="flex gap-6">
<a class="font-label-xs text-on-surface-variant hover:text-primary hover:underline" href="#">Privacy Policy</a>
<a class="font-label-xs text-on-surface-variant hover:text-primary hover:underline" href="#">Terms of Service</a>
<a class="font-label-xs text-on-surface-variant hover:text-primary hover:underline" href="#">Security Compliance</a>
</div>
</footer>
<script>
// Micro-interaction for row selection highlight
document.querySelectorAll('.translation-row').forEach(row => {
row.addEventListener('click', () => {
document.querySelectorAll('.translation-row').forEach(r => r.classList.remove('bg-primary-fixed/10', 'border-l-4', 'border-primary'));
row.classList.add('bg-primary-fixed/10', 'border-l-4', 'border-primary');
});
});
// Simulating some dynamic behavior for AI translation button
const aiBtn = document.querySelector('button:contains("AI 자동 번역")');
if (aiBtn) {
aiBtn.addEventListener('click', function() {
this.innerHTML = `<span class="material-symbols-outlined animate-spin text-[18px]" data-icon="autorenew">autorenew</span> 번역 중...`;
setTimeout(() => {
this.innerHTML = `<span class="material-symbols-outlined text-[18px]" data-icon="auto_awesome" style="font-variation-settings: 'FILL' 1;">auto_awesome</span> AI 자동 번역`;
}, 2000);
});
}
</script>
</body></html>

View File

@ -1,398 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI System - 업체 포털</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"outline": "#727781",
"surface-container-highest": "#dbe2fa",
"surface-container": "#eaedff",
"tertiary": "#003e28",
"outline-variant": "#c2c7d1",
"on-secondary-container": "#f3edff",
"surface-variant": "#dbe2fa",
"on-tertiary-fixed": "#002113",
"primary-fixed": "#d3e4ff",
"on-primary-container": "#91c0ff",
"surface": "#faf8ff",
"primary-fixed-dim": "#a2c9ff",
"on-surface": "#141b2c",
"on-tertiary": "#ffffff",
"on-primary": "#ffffff",
"on-error-container": "#93000a",
"surface-container-low": "#f1f3ff",
"on-surface-variant": "#424750",
"tertiary-container": "#00583b",
"surface-bright": "#f9f9ff",
"error-red": "#ba1a1a",
"error-container": "#ffdad6",
"secondary-fixed-dim": "#c9bfff",
"on-secondary-fixed": "#1b0063",
"surface-tint": "#24609e",
"secondary-fixed": "#e5deff",
"tertiary-fixed-dim": "#72daa9",
"primary": "#003765",
"background": "#faf8ff",
"primary-container": "#004e8b",
"on-tertiary-container": "#68d1a0",
"on-tertiary-fixed-variant": "#005236",
"on-secondary": "#ffffff",
"error": "#ba1a1a",
"inverse-surface": "#293042",
"inverse-on-surface": "#eef0ff",
"surface-container-high": "#e1e7ff",
"tertiary-fixed": "#8ef7c4",
"inverse-primary": "#a2c9ff",
"surface-container-lowest": "#ffffff",
"secondary-container": "#6d4aff",
"on-background": "#141b2c",
"surface-dim": "#d3d9f1",
"on-secondary-fixed-variant": "#4500d8",
"secondary": "#5427e6",
"on-primary-fixed-variant": "#004881",
"on-primary-fixed": "#001c38",
"on-error": "#ffffff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-sm": "8px",
"stack-lg": "24px",
"base": "4px",
"section-padding": "96px",
"container-padding": "24px",
"stack-md": "16px",
"gutter": "16px"
},
"fontFamily": {
"body-sm": ["Inter"],
"label-xs": ["Geist"],
"label-md": ["Geist"],
"title-sm": ["Inter"],
"headline-md": ["Inter"],
"display-lg": ["Inter"],
"display-lg-mobile": ["Inter"],
"body-base": ["Inter"]
},
"fontSize": {
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', 'Pretendard', sans-serif; background-color: #faf8ff; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.glass-effect { backdrop-filter: blur(10px); background: rgba(255, 255, 255, 0.8); }
.sharp-border { border-radius: 4px; }
.no-scrollbar::-webkit-scrollbar { display: none; }
</style>
</head>
<body class="text-on-surface bg-surface-bright">
<!-- TopAppBar -->
<header class="bg-surface-bright dark:bg-inverse-surface border-b border-outline-variant fixed top-0 left-0 right-0 z-50 flex justify-between items-center w-full px-gutter h-16">
<div class="flex items-center gap-4">
<h1 class="font-headline-md text-headline-md text-primary dark:text-primary-fixed-dim tracking-tight">KINTEX AI System</h1>
</div>
<div class="flex items-center gap-6">
<div class="hidden md:flex items-center bg-surface-container-low px-4 py-1.5 rounded-full border border-outline-variant">
<span class="material-symbols-outlined text-outline text-[20px] mr-2">search</span>
<input class="bg-transparent border-none focus:ring-0 text-body-sm w-64 p-0" placeholder="부스 또는 옥션 검색" type="text"/>
</div>
<div class="flex items-center gap-4">
<span class="material-symbols-outlined cursor-pointer text-on-surface-variant hover:text-primary transition-colors">notifications</span>
<div class="flex items-center gap-2 cursor-pointer group">
<span class="material-symbols-outlined text-[32px] text-primary">account_circle</span>
<span class="font-label-md text-label-md text-on-surface-variant group-hover:text-primary">(주)글로벌시공 파트너</span>
</div>
</div>
</div>
</header>
<!-- SideNavBar -->
<aside class="fixed left-0 top-0 h-full w-64 bg-surface-container-low dark:bg-surface-container border-r border-outline-variant flex flex-col pt-16 z-40">
<div class="px-6 py-8">
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 bg-white rounded-md flex items-center justify-center border border-outline-variant">
<img class="w-8 h-8 object-contain" data-alt="A professional, clean vector logo for KINTEX, featuring bold blue typography on a pristine white background. The style is modern corporate, emphasizing authority and architectural precision for a global exhibition center. Soft high-key lighting illuminates the logo subtly." src="https://lh3.googleusercontent.com/aida-public/AB6AXuC7ZnN3zTDsVtirZkqpeVD5c59LDZCAp9BBqiKDLgxw9QWX9cQcVjXZFpfl-dxawoxJluxCX6Qe5WaoYFsfjk5UsLoI2qIEFICAHdy03JKR9Gb4uvP8gyimaQBpmVmRSwZ45W8IDvmajQyrje5TP1zZyEwb_5fBJpQwfVkION2oklft4E3rTiejqQtbNcOpgnZJGV6Wd_XFyNytN-jmN5_WdPlL4M9pMD6IGhvaoIapCDJ78e2UQmNK3eWvG6ggDgLw_Il6Nc2cXKg"/>
</div>
<div>
<p class="font-title-sm text-title-sm text-primary leading-tight">업체 포털</p>
<p class="text-[11px] text-outline uppercase tracking-wider">Construction Mgmt</p>
</div>
</div>
</div>
<nav class="flex-1 px-3 space-y-1">
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant hover:bg-surface-container transition-all" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-primary font-bold border-l-4 border-primary bg-surface-container-high" href="#">
<span class="material-symbols-outlined">storefront</span>
<span class="font-label-md text-label-md">수주 부스</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant hover:bg-surface-container transition-all" href="#">
<span class="material-symbols-outlined">gavel</span>
<span class="font-label-md text-label-md">옥션 참여</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant hover:bg-surface-container transition-all" href="#">
<span class="material-symbols-outlined">payments</span>
<span class="font-label-md text-label-md">정산 관리</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant hover:bg-surface-container transition-all" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-md text-label-md">설정</span>
</a>
</nav>
<div class="p-6">
<div class="bg-primary-container p-4 rounded-xl">
<p class="text-on-primary-container font-label-md mb-2">시공 안전 교육</p>
<div class="w-full bg-white/20 h-1 rounded-full overflow-hidden">
<div class="bg-white h-full w-[85%]"></div>
</div>
<p class="text-white/80 text-[10px] mt-2">D-4 교육 갱신 만료</p>
</div>
</div>
</aside>
<!-- Main Content Canvas -->
<main class="ml-64 pt-16 min-h-screen">
<div class="max-w-[1280px] mx-auto px-gutter py-8">
<!-- KPI Section -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
<div class="bg-surface-container-lowest border border-outline-variant p-4 sharp-border hover:shadow-md transition-shadow">
<p class="text-outline font-label-md mb-1">수주 부스</p>
<h3 class="font-display-lg text-display-lg text-primary">8</h3>
</div>
<div class="bg-surface-container-lowest border border-outline-variant p-4 sharp-border hover:shadow-md transition-shadow">
<p class="text-outline font-label-md mb-1">진행 옥션</p>
<h3 class="font-display-lg text-display-lg text-on-surface">3</h3>
</div>
<div class="bg-surface-container-lowest border border-outline-variant p-4 sharp-border hover:shadow-md transition-shadow">
<p class="text-outline font-label-md mb-1">응찰 대기</p>
<h3 class="font-display-lg text-display-lg text-on-surface">2</h3>
</div>
<div class="bg-surface-container-lowest border border-outline-variant p-4 sharp-border hover:shadow-md transition-shadow">
<p class="text-outline font-label-md mb-1">시공 진행</p>
<h3 class="font-display-lg text-display-lg text-on-tertiary-container">5</h3>
</div>
<div class="bg-error-container border border-error p-4 sharp-border hover:shadow-md transition-shadow">
<p class="text-error-red font-label-md mb-1">마감 임박</p>
<div class="flex items-center justify-between">
<h3 class="font-display-lg text-display-lg text-error-red">1</h3>
<span class="material-symbols-outlined text-error-red animate-pulse">priority_high</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
<!-- Main Left (2/3) - Awarded Booths List -->
<div class="lg:col-span-2 space-y-6">
<div class="flex items-center justify-between mb-4">
<h2 class="font-title-sm text-title-sm text-on-surface">수주 부스 리스트</h2>
<button class="text-primary font-label-md flex items-center gap-1">
전체보기 <span class="material-symbols-outlined text-[16px]">chevron_right</span>
</button>
</div>
<!-- Booth Card 1 -->
<div class="bg-surface-container-lowest border border-outline-variant border-l-4 border-l-primary overflow-hidden sharp-border transition-all hover:scale-[1.01] hover:shadow-lg">
<div class="p-6">
<div class="flex justify-between items-start mb-6">
<div>
<span class="bg-primary-fixed text-on-primary-fixed-variant px-2 py-0.5 rounded text-label-xs mb-2 inline-block">Smart Factory 2026</span>
<h4 class="font-title-sm text-title-sm text-on-surface">스마트팩토리 코리아 2026</h4>
<div class="flex gap-4 mt-1 text-on-surface-variant font-body-sm">
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-[14px]">grid_view</span> Booth A-102</span>
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-[14px]">person</span> (주)한빛로보틱스</span>
</div>
</div>
<div class="text-right">
<p class="text-outline font-label-md">남은 기간</p>
<p class="font-title-sm text-error-red">D-14</p>
</div>
</div>
<!-- Construction Stepper -->
<div class="relative mb-8 px-4">
<div class="absolute top-4 left-4 right-4 h-0.5 bg-surface-container-high z-0"></div>
<div class="relative z-10 flex justify-between">
<div class="flex flex-col items-center">
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-label-md">1</div>
<p class="text-label-md text-primary mt-2">설계</p>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-label-md ring-4 ring-primary-fixed">2</div>
<p class="text-label-md text-primary mt-2 font-bold">규정검증</p>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 rounded-full bg-surface-container-high text-outline flex items-center justify-center font-label-md">3</div>
<p class="text-label-md text-outline mt-2">반입</p>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 rounded-full bg-surface-container-high text-outline flex items-center justify-center font-label-md">4</div>
<p class="text-label-md text-outline mt-2">시공</p>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 rounded-full bg-surface-container-high text-outline flex items-center justify-center font-label-md">5</div>
<p class="text-label-md text-outline mt-2">검수</p>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-2">
<button class="flex-1 py-2.5 bg-primary text-white font-label-md sharp-border hover:bg-primary-container transition-colors flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">brush</span> 설계 스튜디오
</button>
<button class="flex-1 py-2.5 border border-outline-variant text-on-surface font-label-md sharp-border hover:bg-surface-container-low transition-colors flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">description</span> 규정 리포트
</button>
<button class="flex-1 py-2.5 border border-outline-variant text-on-surface font-label-md sharp-border hover:bg-surface-container-low transition-colors flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">event_available</span> 반입 예약
</button>
</div>
</div>
</div>
<!-- Booth Card 2 (Secondary) -->
<div class="bg-surface-container-lowest border border-outline-variant border-l-4 border-l-tertiary-container overflow-hidden sharp-border transition-all opacity-80 hover:opacity-100">
<div class="p-6">
<div class="flex justify-between items-start mb-6">
<div>
<span class="bg-tertiary-fixed text-on-tertiary-fixed-variant px-2 py-0.5 rounded text-label-xs mb-2 inline-block">K-Medical Expo 2026</span>
<h4 class="font-title-sm text-title-sm text-on-surface">K-메디컬 엑스포</h4>
<div class="flex gap-4 mt-1 text-on-surface-variant font-body-sm">
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-[14px]">grid_view</span> Booth D-405</span>
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-[14px]">person</span> 세종메디텍</span>
</div>
</div>
<div class="text-right">
<p class="text-outline font-label-md">상태</p>
<p class="font-label-md text-tertiary-container">시공 예정</p>
</div>
</div>
<div class="relative h-1 bg-surface-container-high rounded-full overflow-hidden">
<div class="absolute top-0 left-0 h-full bg-tertiary-container w-[15%]"></div>
</div>
</div>
</div>
</div>
<!-- Main Right (1/3) - Widgets -->
<div class="space-y-8">
<!-- Auction Participation Widget -->
<div class="bg-surface-container-lowest border border-outline-variant p-6 sharp-border">
<div class="flex items-center justify-between mb-6">
<h2 class="font-label-md text-label-md text-on-surface">옥션 참여 현황</h2>
<span class="material-symbols-outlined text-outline">gavel</span>
</div>
<div class="space-y-4">
<div class="p-3 bg-surface-container-low rounded border border-outline-variant/30 flex justify-between items-center">
<div>
<p class="font-label-md text-on-surface">전기공사 - Hall 1</p>
<p class="text-label-xs text-outline mt-1">입찰가: 24,500,000₩</p>
</div>
<div class="text-right">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-2 py-0.5 rounded text-[10px] font-bold">2위</span>
<p class="text-error-red text-label-xs mt-1">D-2</p>
</div>
</div>
<div class="p-3 bg-surface-container-low rounded border border-outline-variant/30 flex justify-between items-center">
<div>
<p class="font-label-md text-on-surface">네트워크 가설 - Hall 3</p>
<p class="text-label-xs text-outline mt-1">입찰가: 12,000,000₩</p>
</div>
<div class="text-right">
<span class="bg-primary-fixed text-on-primary-fixed-variant px-2 py-0.5 rounded text-[10px] font-bold">1위</span>
<p class="text-on-surface-variant text-label-xs mt-1">D-5</p>
</div>
</div>
<div class="p-3 bg-surface-container-low rounded border border-outline-variant/30 flex justify-between items-center opacity-60">
<div>
<p class="font-label-md text-on-surface">조명 렌탈 - 전시장 전체</p>
<p class="text-label-xs text-outline mt-1">입찰가: 45,000,000₩</p>
</div>
<div class="text-right">
<span class="bg-surface-container-highest text-on-surface-variant px-2 py-0.5 rounded text-[10px] font-bold">5위</span>
<p class="text-on-surface-variant text-label-xs mt-1">D-1</p>
</div>
</div>
</div>
<button class="w-full mt-6 py-2 border-t border-outline-variant text-primary font-label-md hover:bg-surface-container-low transition-colors">
옥션 대시보드로 이동
</button>
</div>
<!-- Notification Feed -->
<div class="bg-surface-container-lowest border border-outline-variant p-6 sharp-border">
<div class="flex items-center justify-between mb-6">
<h2 class="font-label-md text-label-md text-on-surface">알림 피드</h2>
<span class="material-symbols-outlined text-outline">notifications_active</span>
</div>
<div class="space-y-6 relative">
<div class="absolute left-[11px] top-2 bottom-2 w-0.5 bg-outline-variant opacity-30"></div>
<div class="relative pl-8">
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-primary-fixed flex items-center justify-center">
<span class="material-symbols-outlined text-[14px] text-primary">check_circle</span>
</div>
<p class="text-label-md text-on-surface">Booth A-102 규정 검증 완료</p>
<p class="text-label-xs text-outline">방금 전 · KINTEX 운영국</p>
</div>
<div class="relative pl-8">
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-error-container flex items-center justify-center">
<span class="material-symbols-outlined text-[14px] text-error-red">warning</span>
</div>
<p class="text-label-md text-on-surface">안전 교육 갱신 필요 알림</p>
<p class="text-label-xs text-outline">2시간 전 · 시스템</p>
</div>
<div class="relative pl-8">
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-surface-container-highest flex items-center justify-center">
<span class="material-symbols-outlined text-[14px] text-on-surface-variant">info</span>
</div>
<p class="text-label-md text-on-surface">신규 옥션 공고: 제 2전시장 외벽 래핑</p>
<p class="text-label-xs text-outline">어제 · 입찰관리부</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Contextual FAB (Only on relevant screens) -->
<button class="fixed bottom-8 right-8 w-14 h-14 bg-primary text-white rounded-full shadow-lg flex items-center justify-center hover:scale-110 active:scale-95 transition-transform z-50">
<span class="material-symbols-outlined text-[28px]">add</span>
</button>
<script>
// Simple Interaction Logic
document.querySelectorAll('nav a').forEach(link => {
link.addEventListener('click', (e) => {
// e.preventDefault();
// Navigation logic would go here
});
});
// Hover animations for KPI cards
const kpiCards = document.querySelectorAll('.grid-cols-5 > div');
kpiCards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.classList.add('shadow-md');
});
card.addEventListener('mouseleave', () => {
card.classList.remove('shadow-md');
});
});
</script>
</body></html>

View File

@ -1,451 +0,0 @@
<!DOCTYPE html>
<html lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - 업무일지 관리</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-container-low": "#f1f3ff",
"inverse-primary": "#a2c9ff",
"surface-container-lowest": "#ffffff",
"outline": "#727781",
"inverse-surface": "#293042",
"error-red": "#ba1a1a",
"tertiary-container": "#00583b",
"on-primary-fixed": "#001c38",
"on-tertiary": "#ffffff",
"tertiary": "#003e28",
"outline-variant": "#c2c7d1",
"on-tertiary-fixed": "#002113",
"on-primary-fixed-variant": "#004881",
"inverse-on-surface": "#eef0ff",
"primary-container": "#004e8b",
"surface-variant": "#dbe2fa",
"primary": "#003765",
"on-secondary": "#ffffff",
"on-error-container": "#93000a",
"primary-fixed": "#d3e4ff",
"surface-bright": "#f9f9ff",
"secondary-container": "#6d4aff",
"secondary": "#5427e6",
"on-primary-container": "#91c0ff",
"surface-container": "#eaedff",
"error-container": "#ffdad6",
"on-primary": "#ffffff",
"secondary-fixed-dim": "#c9bfff",
"on-secondary-container": "#f3edff",
"on-surface": "#141b2c",
"primary-fixed-dim": "#a2c9ff",
"on-secondary-fixed": "#1b0063",
"on-surface-variant": "#424750",
"on-background": "#141b2c",
"surface-container-highest": "#dbe2fa",
"on-error": "#ffffff",
"on-secondary-fixed-variant": "#4500d8",
"tertiary-fixed-dim": "#72daa9",
"on-tertiary-fixed-variant": "#005236",
"surface-tint": "#24609e",
"error": "#ba1a1a",
"surface-container-high": "#e1e7ff",
"surface": "#faf8ff",
"tertiary-fixed": "#8ef7c4",
"surface-dim": "#d3d9f1",
"on-tertiary-container": "#68d1a0",
"secondary-fixed": "#e5deff",
"background": "#faf8ff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"gutter": "16px",
"container-padding": "24px",
"base": "4px",
"stack-sm": "8px",
"stack-md": "16px",
"stack-lg": "24px",
"section-padding": "96px"
},
"fontFamily": {
"headline-md": ["Inter"],
"label-md": ["Geist"],
"display-lg": ["Inter"],
"body-base": ["Inter"],
"label-xs": ["Geist"],
"title-sm": ["Inter"],
"body-sm": ["Inter"]
},
"fontSize": {
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', 'Pretendard', sans-serif; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.table-zebra tr:nth-child(even) { background-color: #f1f3ff; }
.sticky-header { position: sticky; top: 0; z-index: 10; }
</style>
</head>
<body class="bg-surface-bright text-on-surface">
<!-- Sidebar (Predicted: SideNavBar) -->
<aside class="fixed left-0 top-0 h-screen w-[240px] bg-surface-container-lowest border-r border-outline-variant flex flex-col py-stack-lg z-50">
<div class="px-6 mb-8">
<h1 class="font-display-lg text-display-lg text-primary tracking-tight">KINTEX AI</h1>
<p class="text-label-xs text-on-surface-variant uppercase tracking-widest">Exhibition CMS</p>
</div>
<nav class="flex-1 space-y-1">
<div class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200 cursor-pointer active:opacity-80">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
<span class="font-body-base text-body-base">대시보드</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200 cursor-pointer active:opacity-80">
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
<span class="font-body-base text-body-base">전시 관리</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200 cursor-pointer active:opacity-80">
<span class="material-symbols-outlined" data-icon="meeting_room">meeting_room</span>
<span class="font-body-base text-body-base">시설 관리</span>
</div>
<!-- ACTIVE: Worklog -->
<div class="flex items-center gap-3 px-4 py-3 bg-secondary-fixed text-on-secondary-fixed font-bold border-l-4 border-primary cursor-pointer">
<span class="material-symbols-outlined" data-icon="edit_note" style="font-variation-settings: 'FILL' 1;">edit_note</span>
<span class="font-body-base text-body-base">업무일지</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200 cursor-pointer active:opacity-80">
<span class="material-symbols-outlined" data-icon="query_stats">query_stats</span>
<span class="font-body-base text-body-base">통계 분석</span>
</div>
<div class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200 cursor-pointer active:opacity-80">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
<span class="font-body-base text-body-base">설정</span>
</div>
</nav>
<div class="mt-auto px-4">
<button class="w-full py-3 bg-primary text-on-primary rounded-lg flex items-center justify-center gap-2 font-bold hover:bg-primary-container transition-all">
<span class="material-symbols-outlined" data-icon="add">add</span>
<span>New Event</span>
</button>
</div>
</aside>
<!-- Top Navigation (Predicted: TopNavBar) -->
<header class="fixed top-0 right-0 left-[240px] h-16 border-b border-outline-variant bg-surface-bright/80 backdrop-blur-md flex justify-between items-center px-gutter z-40">
<div class="flex items-center gap-4 flex-1">
<div class="relative w-full max-w-md">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant" data-icon="search">search</span>
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm focus:outline-none focus:border-primary" placeholder="검색어를 입력하세요..." type="text"/>
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors" data-icon="notifications">notifications</span>
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors" data-icon="help">help</span>
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors" data-icon="apps">apps</span>
</div>
<div class="h-8 w-px bg-outline-variant"></div>
<div class="flex items-center gap-3">
<div class="text-right">
<p class="font-label-md text-label-md text-on-surface">Admin User</p>
<p class="text-[10px] text-on-surface-variant leading-none">시설지원팀 매니저</p>
</div>
<div class="w-10 h-10 rounded-full bg-secondary-fixed flex items-center justify-center overflow-hidden border border-outline-variant">
<img class="w-full h-full object-cover" data-alt="A professional headshot of a Korean male office administrator in his 40s wearing a crisp white shirt and a company ID badge. He has a friendly yet professional expression. The background is a blurred office environment with corporate branding. The lighting is soft and natural, emphasizing high-fidelity corporate UI standards." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDdSzDOeFTOmTw-nGTeYpZnloLNWg9KQIcaw01QWfQj-AIUOFLrvZmhIX--2TMzMJppRWJVmCeETYiR9wHHbjOlM5Km6gj5MwUqfGWF5W71-nnDQb5Hlb8aoZzcxOyrgWVR2eOnjyvYLXx8g9zDZj0YH-Zugxk7EbGZH--k94m_SAxGnrzhf_ahstYaOQrl-vOg_W3YBZi3HcU6e6zHXqfzuK6vTqEhodJq2q-uzDSFi1dgO4C4JfmuW_QYZt5XBL0btF26onoEyIE"/>
</div>
</div>
</div>
</header>
<!-- Main Workspace -->
<main class="ml-[240px] pt-16 h-screen flex">
<!-- Worklog List (Left 2/3) -->
<section class="flex-1 flex flex-col p-stack-lg overflow-hidden">
<header class="mb-6 flex justify-between items-end">
<div>
<h2 class="font-headline-md text-headline-md text-primary">업무일지 관리</h2>
<p class="text-body-sm text-on-surface-variant">킨텍스 시설 및 전시 홀 운영 관련 일일 업무 현황</p>
</div>
<div class="flex gap-2">
<button class="px-4 py-2 border border-outline-variant rounded-lg flex items-center gap-2 text-body-sm font-medium hover:bg-surface-container-low transition-colors">
<span class="material-symbols-outlined text-[18px]" data-icon="download">download</span>
엑셀 다운로드
</button>
<button class="px-4 py-2 bg-primary text-on-primary rounded-lg flex items-center gap-2 text-body-sm font-bold shadow-md hover:bg-primary-container transition-all">
<span class="material-symbols-outlined text-[18px]" data-icon="edit">edit</span>
일지 작성
</button>
</div>
</header>
<!-- Filter Bar -->
<div class="bg-surface-container-lowest border border-outline-variant p-4 rounded-xl flex items-center gap-stack-md mb-stack-lg shadow-sm">
<div class="flex items-center gap-2 border-r border-outline-variant pr-4">
<span class="text-label-xs text-on-surface-variant font-bold uppercase">기간</span>
<input class="border-0 bg-transparent text-body-sm focus:ring-0 p-1 cursor-pointer" type="date"/>
<span class="text-on-surface-variant">-</span>
<input class="border-0 bg-transparent text-body-sm focus:ring-0 p-1 cursor-pointer" type="date"/>
</div>
<div class="flex items-center gap-2 border-r border-outline-variant pr-4">
<span class="text-label-xs text-on-surface-variant font-bold uppercase">작성자</span>
<select class="border-0 bg-transparent text-body-sm focus:ring-0 p-1 cursor-pointer pr-8">
<option>전체</option>
<option>홍길동</option>
<option>이철수</option>
<option>김영희</option>
</select>
</div>
<div class="flex items-center gap-2 flex-1">
<span class="text-label-xs text-on-surface-variant font-bold uppercase">유형</span>
<div class="flex gap-1">
<span class="px-3 py-1 bg-primary text-on-primary rounded-full text-label-xs cursor-pointer">전체</span>
<span class="px-3 py-1 border border-outline-variant text-on-surface-variant rounded-full text-label-xs hover:bg-surface-container-low cursor-pointer">일반</span>
<span class="px-3 py-1 border border-outline-variant text-on-surface-variant rounded-full text-label-xs hover:bg-surface-container-low cursor-pointer">긴급</span>
<span class="px-3 py-1 border border-outline-variant text-on-surface-variant rounded-full text-label-xs hover:bg-surface-container-low cursor-pointer">정기</span>
</div>
</div>
<button class="px-4 py-2 bg-secondary text-on-secondary rounded-lg text-body-sm font-bold">조회</button>
</div>
<!-- Table Container -->
<div class="flex-1 bg-surface-container-lowest border border-outline-variant rounded-xl overflow-auto shadow-sm">
<table class="w-full text-left border-collapse table-zebra">
<thead class="sticky-header bg-surface-container-high">
<tr class="h-[44px]">
<th class="px-4 py-2 font-label-md text-label-md text-on-surface border-b border-outline-variant w-32">날짜</th>
<th class="px-4 py-2 font-label-md text-label-md text-on-surface border-b border-outline-variant">제목</th>
<th class="px-4 py-2 font-label-md text-label-md text-on-surface border-b border-outline-variant w-24 text-center">유형</th>
<th class="px-4 py-2 font-label-md text-label-md text-on-surface border-b border-outline-variant w-28 text-center">진행상태</th>
<th class="px-4 py-2 font-label-md text-label-md text-on-surface border-b border-outline-variant w-20 text-right">공수</th>
</tr>
</thead>
<tbody class="text-body-sm">
<!-- Row 1 -->
<tr class="h-[44px] hover:bg-primary-fixed/20 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary">
<td class="px-4 py-2 text-on-surface-variant font-label-md">2024-05-24</td>
<td class="px-4 py-2 font-medium">제1전시장 3홀 냉방 공조 시스템 필터 교체 작업</td>
<td class="px-4 py-2 text-center">
<span class="bg-tertiary-fixed text-on-tertiary-fixed-variant px-2 py-0.5 rounded text-[11px] font-bold">정기</span>
</td>
<td class="px-4 py-2 text-center">
<span class="bg-surface-container-highest text-on-surface-variant px-2 py-0.5 rounded-full text-[11px] font-bold">진행중</span>
</td>
<td class="px-4 py-2 text-right">4.0h</td>
</tr>
<!-- Row 2 -->
<tr class="h-[44px] hover:bg-primary-fixed/20 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary">
<td class="px-4 py-2 text-on-surface-variant font-label-md">2024-05-24</td>
<td class="px-4 py-2 font-medium">야외 주차장 구역 조명 램프 불량 교체 (C구역)</td>
<td class="px-4 py-2 text-center">
<span class="bg-error-container text-on-error-container px-2 py-0.5 rounded text-[11px] font-bold">긴급</span>
</td>
<td class="px-4 py-2 text-center">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-2 py-0.5 rounded-full text-[11px] font-bold">완료</span>
</td>
<td class="px-4 py-2 text-right">1.5h</td>
</tr>
<!-- Row 3 -->
<tr class="h-[44px] hover:bg-primary-fixed/20 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary">
<td class="px-4 py-2 text-on-surface-variant font-label-md">2024-05-23</td>
<td class="px-4 py-2 font-medium">2024 서울 모빌리티 쇼 전력 부하 모니터링</td>
<td class="px-4 py-2 text-center">
<span class="bg-primary-fixed text-on-primary-fixed-variant px-2 py-0.5 rounded text-[11px] font-bold">일반</span>
</td>
<td class="px-4 py-2 text-center">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-2 py-0.5 rounded-full text-[11px] font-bold">완료</span>
</td>
<td class="px-4 py-2 text-right">8.0h</td>
</tr>
<!-- Row 4 -->
<tr class="h-[44px] hover:bg-primary-fixed/20 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary">
<td class="px-4 py-2 text-on-surface-variant font-label-md">2024-05-23</td>
<td class="px-4 py-2 font-medium">제2전시장 7-8홀 화장실 수전 누수 점검 및 보수</td>
<td class="px-4 py-2 text-center">
<span class="bg-tertiary-fixed text-on-tertiary-fixed-variant px-2 py-0.5 rounded text-[11px] font-bold">정기</span>
</td>
<td class="px-4 py-2 text-center">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-2 py-0.5 rounded-full text-[11px] font-bold">완료</span>
</td>
<td class="px-4 py-2 text-right">2.0h</td>
</tr>
<!-- Row 5 -->
<tr class="h-[44px] hover:bg-primary-fixed/20 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary">
<td class="px-4 py-2 text-on-surface-variant font-label-md">2024-05-22</td>
<td class="px-4 py-2 font-medium">그랜드볼룸 오디오 시스템 무선 마이크 수신 테스트</td>
<td class="px-4 py-2 text-center">
<span class="bg-primary-fixed text-on-primary-fixed-variant px-2 py-0.5 rounded text-[11px] font-bold">일반</span>
</td>
<td class="px-4 py-2 text-center">
<span class="bg-secondary-fixed text-on-secondary-fixed-variant px-2 py-0.5 rounded-full text-[11px] font-bold">완료</span>
</td>
<td class="px-4 py-2 text-right">3.0h</td>
</tr>
<!-- Row 6 -->
<tr class="h-[44px] hover:bg-primary-fixed/20 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary">
<td class="px-4 py-2 text-on-surface-variant font-label-md">2024-05-22</td>
<td class="px-4 py-2 font-medium">전시장 바닥 마감재 크랙 보수 (5홀 입구측)</td>
<td class="px-4 py-2 text-center">
<span class="bg-tertiary-fixed text-on-tertiary-fixed-variant px-2 py-0.5 rounded text-[11px] font-bold">정기</span>
</td>
<td class="px-4 py-2 text-center">
<span class="bg-surface-container-highest text-on-surface-variant px-2 py-0.5 rounded-full text-[11px] font-bold">진행중</span>
</td>
<td class="px-4 py-2 text-right">4.5h</td>
</tr>
</tbody>
</table>
</div>
<footer class="mt-4 flex justify-between items-center px-2">
<div class="text-label-xs text-on-surface-variant">Showing 1-6 of 128 results</div>
<div class="flex gap-1">
<button class="w-8 h-8 rounded border border-outline-variant flex items-center justify-center hover:bg-surface-container-low"><span class="material-symbols-outlined text-[16px]">chevron_left</span></button>
<button class="w-8 h-8 rounded bg-primary text-on-primary font-bold text-label-xs">1</button>
<button class="w-8 h-8 rounded border border-outline-variant text-label-xs hover:bg-surface-container-low">2</button>
<button class="w-8 h-8 rounded border border-outline-variant text-label-xs hover:bg-surface-container-low">3</button>
<button class="w-8 h-8 rounded border border-outline-variant flex items-center justify-center hover:bg-surface-container-low"><span class="material-symbols-outlined text-[16px]">chevron_right</span></button>
</div>
</footer>
</section>
<!-- Detail/Compose Panel (Right 1/3) -->
<aside class="w-[440px] bg-surface-container-lowest border-l border-outline-variant flex flex-col shadow-2xl z-30">
<header class="p-stack-lg border-b border-outline-variant flex justify-between items-center">
<h3 class="font-title-sm text-title-sm text-on-surface">업무일지 상세/작성</h3>
<div class="flex gap-2">
<button class="px-3 py-1.5 border border-error-red text-error-red rounded-lg text-label-md font-bold hover:bg-error-container transition-colors">삭제</button>
<button class="px-3 py-1.5 bg-primary text-on-primary rounded-lg text-label-md font-bold shadow hover:bg-primary-container transition-all">저장</button>
</div>
</header>
<div class="flex-1 overflow-y-auto p-stack-lg space-y-stack-md scrollbar-hide">
<div class="space-y-1">
<label class="text-label-xs font-bold text-on-surface-variant uppercase">날짜</label>
<input class="w-full border border-outline-variant rounded-lg p-2.5 text-body-base bg-surface-bright focus:ring-1 focus:ring-primary focus:border-primary" type="date" value="2024-05-24"/>
</div>
<div class="space-y-1">
<label class="text-label-xs font-bold text-on-surface-variant uppercase">제목</label>
<input class="w-full border border-outline-variant rounded-lg p-2.5 text-body-base bg-surface-bright focus:ring-1 focus:ring-primary focus:border-primary" type="text" value="제1전시장 3홀 냉방 공조 시스템 필터 교체 작업"/>
</div>
<div class="grid grid-cols-2 gap-stack-md">
<div class="space-y-1">
<label class="text-label-xs font-bold text-on-surface-variant uppercase">유형</label>
<select class="w-full border border-outline-variant rounded-lg p-2.5 text-body-base bg-surface-bright focus:ring-1 focus:ring-primary focus:border-primary">
<option>일반</option>
<option>긴급</option>
<option selected="">정기</option>
</select>
</div>
<div class="space-y-1">
<label class="text-label-xs font-bold text-on-surface-variant uppercase">공수 (Hours)</label>
<div class="relative">
<input class="w-full border border-outline-variant rounded-lg p-2.5 pr-10 text-body-base bg-surface-bright focus:ring-1 focus:ring-primary focus:border-primary" step="0.5" type="number" value="4.0"/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-label-md">h</span>
</div>
</div>
</div>
<div class="space-y-1">
<label class="text-label-xs font-bold text-on-surface-variant uppercase">주요 이슈</label>
<textarea class="w-full border border-outline-variant rounded-lg p-2.5 text-body-base bg-surface-bright focus:ring-1 focus:ring-primary focus:border-primary resize-none" placeholder="작업 내용 및 발생한 특이사항을 상세히 기록하세요." rows="6">3홀 북동측 공조기(AHU-03)의 헤파 필터 오염도가 기준치를 초과하여 신규 필터로 교체 완료함. 작업 중 댐퍼 구동부 소음이 발견되어 윤활유 보충 조치 하였으나, 차주 정밀 점검이 필요해 보임. 교체 후 풍량 및 온도 센서 정상 작동 확인.</textarea>
</div>
<!-- Media Attachments Preview (Custom Addition for Fidelity) -->
<div class="space-y-1 pt-2">
<label class="text-label-xs font-bold text-on-surface-variant uppercase">첨부 파일</label>
<div class="grid grid-cols-3 gap-2">
<div class="aspect-square rounded-lg border border-outline-variant overflow-hidden bg-surface-container relative group">
<img class="w-full h-full object-cover" data-alt="A macro shot of a dirty industrial air filter being replaced by a technician in a sterile, professional maintenance environment. Soft blue work lighting, high quality industrial photography." src="https://lh3.googleusercontent.com/aida-public/AB6AXuB55qKhoiNURXmGZhubeuwELEM9_hP82kfpByxvt4UFMnTz4a6uD4ce2tbC6BJyg9Yb57fHM131Xs0Jxv6-1pDw77X-3KLSjdQCGLD6YVecU1_BBEyV3kN6L5gkV6rnA9YOUdrMe4rvG9KLDuYD1mLHgp_cUUNnR18KOii9Gi6_g9JgSsNS3w52BfHjfPKQL23tJ-mEqpOq2ri8OuHX6JIjHhHVNKnrRzW2YxaCMDbw2J3KiD6FntVHwka9TjcBKw6stCxk3uxvoEA"/>
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity cursor-pointer">
<span class="material-symbols-outlined text-white" data-icon="visibility">visibility</span>
</div>
</div>
<div class="aspect-square rounded-lg border border-dashed border-outline-variant flex flex-col items-center justify-center text-on-surface-variant cursor-pointer hover:bg-surface-container-low">
<span class="material-symbols-outlined" data-icon="add_a_photo">add_a_photo</span>
<span class="text-[10px] mt-1">Add Photo</span>
</div>
</div>
</div>
<!-- Comment Section -->
<div class="pt-6 border-t border-outline-variant space-y-4">
<h4 class="text-label-md font-bold text-on-surface flex items-center gap-2">
<span class="material-symbols-outlined text-[18px]" data-icon="comment">comment</span>
히스토리 및 코멘트
</h4>
<div class="space-y-stack-md">
<!-- Comment 1 -->
<div class="flex gap-3">
<div class="w-8 h-8 rounded-full bg-primary-fixed flex-shrink-0 flex items-center justify-center text-primary font-bold text-[10px]">LS</div>
<div class="flex-1 bg-surface-container-low p-3 rounded-tr-xl rounded-br-xl rounded-bl-xl">
<div class="flex justify-between items-center mb-1">
<span class="text-label-xs font-bold">이승우 팀장</span>
<span class="text-[10px] text-on-surface-variant">2시간 전</span>
</div>
<p class="text-body-sm">윤활유 조치 후에도 소음 지속될 경우 부품 교체 품의 부탁드립니다. 고생하셨습니다.</p>
</div>
</div>
<!-- Comment 2 -->
<div class="flex gap-3">
<div class="w-8 h-8 rounded-full bg-secondary-fixed flex-shrink-0 flex items-center justify-center text-secondary font-bold text-[10px]">KH</div>
<div class="flex-1 bg-surface-container-low p-3 rounded-tr-xl rounded-br-xl rounded-bl-xl">
<div class="flex justify-between items-center mb-1">
<span class="text-label-xs font-bold">김현아 매니저</span>
<span class="text-[10px] text-on-surface-variant">1시간 전</span>
</div>
<p class="text-body-sm">차주 정밀 점검 일정은 월요일 오전 10시로 잡아두었습니다.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Comment Input -->
<div class="p-stack-md border-t border-outline-variant bg-surface-bright">
<div class="relative">
<textarea class="w-full border border-outline-variant rounded-xl p-3 pr-12 text-body-sm bg-surface-container-lowest focus:ring-1 focus:ring-primary focus:border-primary resize-none h-12" placeholder="의견을 남겨주세요..."></textarea>
<button class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 bg-primary text-on-primary rounded-lg flex items-center justify-center shadow hover:bg-primary-container transition-all">
<span class="material-symbols-outlined text-[18px]" data-icon="send" style="font-variation-settings: 'FILL' 1;">send</span>
</button>
</div>
</div>
</aside>
</main>
<!-- Micro-interactions Script -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// Table row click simulation
const rows = document.querySelectorAll('tbody tr');
rows.forEach(row => {
row.addEventListener('click', () => {
// Update UI to show selected state (visual feedback)
rows.forEach(r => r.classList.remove('bg-primary-fixed/20', 'border-primary'));
row.classList.add('bg-primary-fixed/20', 'border-primary');
// In a real app, this would load the data into the right panel
console.log('Worklog entry selected:', row.cells[1].textContent);
});
});
// Input focus animations
const inputs = document.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
input.addEventListener('focus', () => {
input.parentElement.querySelector('label')?.classList.add('text-primary');
});
input.addEventListener('blur', () => {
input.parentElement.querySelector('label')?.classList.remove('text-primary');
});
});
});
</script>
</body></html>

View File

@ -1,489 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - 통합 일정 관리</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#8ef7c4",
"secondary-fixed": "#e5deff",
"primary-fixed-dim": "#a2c9ff",
"on-surface": "#141b2c",
"inverse-on-surface": "#eef0ff",
"surface-dim": "#d3d9f1",
"secondary-container": "#6d4aff",
"surface-variant": "#dbe2fa",
"background": "#faf8ff",
"on-secondary-fixed-variant": "#4500d8",
"surface": "#faf8ff",
"secondary-fixed-dim": "#c9bfff",
"surface-container-low": "#f2f3ff",
"surface-container-highest": "#dbe2fa",
"tertiary-container": "#00583b",
"surface-tint": "#24609e",
"surface-container": "#eaedff",
"outline-variant": "#c2c7d1",
"on-primary": "#ffffff",
"primary-fixed": "#d3e4ff",
"on-primary-fixed-variant": "#004881",
"tertiary": "#003e28",
"on-surface-variant": "#424750",
"primary-container": "#004e8b",
"error-red": "#ba1a1a",
"inverse-surface": "#293042",
"surface-container-lowest": "#ffffff",
"on-tertiary-fixed-variant": "#005236",
"inverse-primary": "#a2c9ff",
"on-error-container": "#93000a",
"primary": "#003765",
"on-tertiary-container": "#68d1a0",
"on-tertiary-fixed": "#002113",
"outline": "#727781",
"on-primary-container": "#91c0ff",
"surface-container-high": "#e1e7ff",
"error": "#ba1a1a",
"on-tertiary": "#ffffff",
"on-secondary-fixed": "#1b0063",
"secondary": "#5427e6",
"on-background": "#141b2c",
"surface-bright": "#faf8ff",
"tertiary-fixed-dim": "#72daa9",
"on-error": "#ffffff",
"on-primary-fixed": "#001c38",
"on-secondary": "#ffffff",
"error-container": "#ffdad6",
"on-secondary-container": "#f3edff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "24px",
"container-padding": "24px",
"base": "4px",
"stack-md": "16px",
"stack-sm": "8px",
"gutter": "16px",
"section-padding": "96px"
},
"fontFamily": {
"title-sm": ["Inter"],
"display-lg-mobile": ["Inter"],
"headline-md": ["Inter"],
"body-sm": ["Inter"],
"body-base": ["Inter"],
"label-xs": ["Geist"],
"label-md": ["Geist"],
"display-lg": ["Inter"]
},
"fontSize": {
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body {
background-color: #faf8ff;
color: #141b2c;
font-family: 'Inter', 'Geist', sans-serif;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: minmax(120px, 1fr);
}
.calendar-cell {
border-right: 1px solid #c2c7d1;
border-bottom: 1px solid #c2c7d1;
}
.glass-overlay {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
</style>
</head>
<body class="overflow-hidden">
<!-- SideNavBar -->
<aside class="w-[240px] h-screen fixed left-0 top-0 flex flex-col py-stack-lg bg-surface border-r border-outline-variant z-50">
<div class="px-6 mb-8 flex items-center gap-3">
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center text-on-primary">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">hub</span>
</div>
<div>
<h1 class="font-headline-md text-headline-md font-bold text-primary leading-tight">KINTEX AI</h1>
<p class="text-label-md font-label-md text-on-surface-variant">전시관리 시스템</p>
</div>
</div>
<nav class="flex-1 px-3 space-y-1">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-primary border-l-4 border-primary bg-primary-fixed transition-colors" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">calendar_month</span>
<span class="font-label-md text-label-md">Exhibition Schedule</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">groups</span>
<span class="font-label-md text-label-md">Team Calendar</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">map</span>
<span class="font-label-md text-label-md">Venue Map</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">account_tree</span>
<span class="font-label-md text-label-md">Booth Management</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">query_stats</span>
<span class="font-label-md text-label-md">Analytics</span>
</a>
</nav>
<div class="px-6 mb-4">
<button class="w-full py-3 bg-primary text-on-primary rounded-xl font-label-md text-label-md flex items-center justify-center gap-2 shadow-md hover:opacity-90 transition-opacity">
<span class="material-symbols-outlined text-sm">add</span>
New Exhibition
</button>
</div>
<div class="px-3 border-t border-outline-variant pt-4 space-y-1">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-md text-label-md">Settings</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-high transition-colors" href="#">
<span class="material-symbols-outlined">help_outline</span>
<span class="font-label-md text-label-md">Help Center</span>
</a>
</div>
</aside>
<!-- TopAppBar -->
<header class="flex justify-between items-center h-16 px-gutter w-[calc(100%-240px)] ml-[240px] bg-surface-bright sticky top-0 border-b border-outline-variant z-40">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-on-surface-variant font-body-base text-body-base">
<span>일정 관리</span>
<span class="material-symbols-outlined text-sm">chevron_right</span>
<span class="font-bold text-on-surface">통합 일정</span>
</div>
<div class="h-6 w-px bg-outline-variant mx-2"></div>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-3 text-on-surface-variant text-[20px]">search</span>
<input class="pl-10 pr-4 py-1.5 bg-surface-container-low border border-outline-variant rounded-full text-body-sm w-64 focus:ring-2 focus:ring-primary focus:outline-none transition-all" placeholder="행사명 또는 일정 검색" type="text"/>
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex gap-6">
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-base text-body-base" href="#">Overview</a>
<a class="text-primary font-bold border-b-2 border-primary pb-1 transition-colors font-body-base text-body-base" href="#">Reports</a>
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-base text-body-base" href="#">Inventory</a>
</div>
<div class="flex items-center gap-4">
<button class="text-on-surface-variant hover:bg-surface-container p-2 rounded-full transition-colors relative">
<span class="material-symbols-outlined">notifications</span>
<span class="absolute top-1.5 right-1.5 w-2 h-2 bg-error rounded-full"></span>
</button>
<button class="text-on-surface-variant hover:bg-surface-container p-2 rounded-full transition-colors">
<span class="material-symbols-outlined">apps</span>
</button>
<div class="flex items-center gap-3 ml-2 cursor-pointer hover:bg-surface-container p-1 rounded-lg transition-colors">
<div class="text-right">
<p class="font-title-sm text-[12px] leading-tight text-on-surface">관리자 센터</p>
<p class="text-label-xs text-on-surface-variant">Admin Profile</p>
</div>
<img class="w-9 h-9 rounded-full object-cover border border-outline-variant" data-alt="A professional headshot of a Korean male executive in his 40s, wearing a modern charcoal suit and glasses. He has a friendly but authoritative expression, posed against a blurred office background with cool lighting. The image style is crisp, clean, and corporate, matching the high-end KINTEX enterprise UI." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBTpg-HNyFtLE30U8EyhTmr9MJq8wnTEI98vPeIQ7JbcVo8AbRXYFUpR7cAXGSc-leUYsBqf_5sA8kxKzrIYHZTMvDmvq9c6J43UWA_bMrRbAUIJJTD6tWXpi0O3ugHzpKFm8Bt3UwMi5nr2hrxX5NOP2wZC10nAT_Pdhvc1jO6iFmiFzBHElPOLZwQv5_YkMHr8S3damK63qGvEo6Sf1K8az7BkCTbN3O-g8McAU355md88yptQekHdcCQ3r8V8EOSMmdTRDOgG64"/>
</div>
</div>
</div>
</header>
<!-- Main Content Cluster -->
<main class="ml-[240px] w-[calc(100%-240px)] h-[calc(100vh-64px)] flex overflow-hidden">
<!-- Calendar Section -->
<section class="flex-1 flex flex-col bg-white border-r border-outline-variant overflow-y-auto no-scrollbar">
<!-- Calendar Header -->
<div class="p-6 flex items-center justify-between border-b border-outline-variant bg-white">
<div class="flex items-center gap-8">
<div class="flex items-center gap-4">
<h2 class="font-display-lg text-display-lg text-primary tracking-tight">2026년 8월</h2>
<div class="flex border border-outline-variant rounded-lg overflow-hidden shadow-sm">
<button class="p-2 hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">chevron_left</span></button>
<button class="px-4 text-label-md font-label-md border-x border-outline-variant hover:bg-surface-container transition-colors">오늘</button>
<button class="p-2 hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">chevron_right</span></button>
</div>
</div>
<!-- Legend -->
<div class="flex gap-4">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-primary"></span>
<span class="text-label-md font-label-md text-on-surface-variant">행사 마일스톤</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-secondary"></span>
<span class="text-label-md font-label-md text-on-surface-variant">옥션 마감</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-tertiary-container"></span>
<span class="text-label-md font-label-md text-on-surface-variant">반입 슬롯</span>
</div>
</div>
</div>
<!-- View Toggles -->
<div class="flex bg-surface-container-low p-1 rounded-lg border border-outline-variant">
<button class="px-4 py-1.5 rounded-md text-label-md font-label-md bg-white shadow-sm text-primary"></button>
<button class="px-4 py-1.5 rounded-md text-label-md font-label-md text-on-surface-variant hover:bg-white transition-all"></button>
<button class="px-4 py-1.5 rounded-md text-label-md font-label-md text-on-surface-variant hover:bg-white transition-all"></button>
</div>
</div>
<!-- Calendar Body -->
<div class="calendar-grid flex-1 border-l border-t border-outline-variant bg-surface-container-lowest">
<!-- Day Labels -->
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-error-red bg-surface-container-low border-r border-b border-outline-variant"></div>
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-on-surface-variant bg-surface-container-low border-r border-b border-outline-variant"></div>
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-on-surface-variant bg-surface-container-low border-r border-b border-outline-variant"></div>
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-on-surface-variant bg-surface-container-low border-r border-b border-outline-variant"></div>
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-on-surface-variant bg-surface-container-low border-r border-b border-outline-variant"></div>
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-on-surface-variant bg-surface-container-low border-r border-b border-outline-variant"></div>
<div class="h-10 flex items-center justify-center font-label-md text-label-md text-primary bg-surface-container-low border-r border-b border-outline-variant"></div>
<!-- Empty Cells for start of month (August 2026 starts on Saturday) -->
<div class="calendar-cell bg-surface-container-lowest opacity-40"></div>
<div class="calendar-cell bg-surface-container-lowest opacity-40"></div>
<div class="calendar-cell bg-surface-container-lowest opacity-40"></div>
<div class="calendar-cell bg-surface-container-lowest opacity-40"></div>
<div class="calendar-cell bg-surface-container-lowest opacity-40"></div>
<div class="calendar-cell bg-surface-container-lowest opacity-40"></div>
<!-- Days -->
<div class="calendar-cell p-2">
<span class="text-label-md text-primary">1</span>
</div>
<div class="calendar-cell p-2 bg-error-container/10">
<span class="text-label-md text-error-red">2</span>
</div>
<div class="calendar-cell p-2">
<span class="text-label-md text-on-surface-variant">3</span>
<div class="mt-1 space-y-1">
<div class="bg-primary-fixed px-2 py-0.5 rounded text-[10px] font-bold text-primary truncate">글로벌 무역 전시회</div>
</div>
</div>
<!-- Highlighted Active Day -->
<div class="calendar-cell p-2 bg-primary-fixed/30 ring-2 ring-primary ring-inset z-10">
<div class="flex justify-between items-start">
<span class="text-label-md font-bold text-primary">4</span>
<span class="material-symbols-outlined text-primary text-[14px]">push_pin</span>
</div>
<div class="mt-1 space-y-1">
<div class="bg-secondary px-2 py-1 rounded text-[10px] font-bold text-on-secondary truncate">1홀 전기공사 옥션 마감</div>
<div class="bg-tertiary-container px-2 py-1 rounded text-[10px] font-bold text-on-tertiary truncate">환경 개선 정비</div>
</div>
</div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">5</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">6</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">7</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-primary">8</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-error-red">9</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">10</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">11</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">12</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">13</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">14</span></div>
<div class="calendar-cell p-2 bg-error-container/20"><span class="text-label-md text-error-red font-bold">15 (광복절)</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-error-red">16</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">17</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">18</span></div>
<div class="calendar-cell p-2">
<span class="text-label-md text-on-surface-variant">19</span>
<div class="mt-1">
<div class="bg-tertiary-fixed-dim px-2 py-1 rounded text-[10px] font-bold text-on-tertiary-fixed truncate border-l-4 border-tertiary">글로벌 무역상사 반입</div>
</div>
</div>
<!-- ... more cells -->
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">20</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">21</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-primary">22</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-error-red">23</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">24</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">25</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">26</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">27</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">28</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-primary">29</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-error-red">30</span></div>
<div class="calendar-cell p-2"><span class="text-label-md text-on-surface-variant">31</span></div>
</div>
<!-- Footer Notice -->
<footer class="p-4 bg-surface-container-low border-t border-outline-variant flex items-center justify-between">
<div class="flex items-center gap-3 text-body-sm">
<span class="material-symbols-outlined text-error-red">info</span>
<p class="text-on-surface-variant">다가오는 공휴일 안내: 8월 15일은 광복절로 시스템 점검이 예정되어 있습니다. 원활한 전시 관리를 위해 일정을 미리 확인해 주세요.</p>
</div>
<div class="flex gap-4">
<button class="text-label-md font-label-md text-primary hover:underline">상세 공지사항</button>
<button class="text-label-md font-label-md text-primary hover:underline">시스템 도움말</button>
</div>
</footer>
</section>
<!-- Right Agenda Panel -->
<aside class="w-[400px] flex flex-col bg-surface border-l border-outline-variant overflow-y-auto no-scrollbar">
<div class="p-6 sticky top-0 bg-surface z-20 border-b border-outline-variant">
<div class="flex justify-between items-center mb-6">
<div>
<h3 class="font-headline-md text-headline-md text-on-surface">2026. 08. 04 (화)</h3>
<p class="text-label-md font-label-md text-on-surface-variant">선택된 날짜의 상세 일정입니다.</p>
</div>
<button class="w-12 h-12 rounded-full bg-primary-fixed-dim text-primary flex items-center justify-center hover:bg-primary-fixed transition-colors shadow-sm">
<span class="material-symbols-outlined">filter_list</span>
</button>
</div>
<button class="w-full py-4 bg-primary text-on-primary rounded-xl font-bold flex items-center justify-center gap-2 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all">
<span class="material-symbols-outlined">add_circle</span>
+ 일정 추가
</button>
</div>
<div class="p-6 space-y-gutter">
<!-- Agenda Card 1 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-stack-md flex gap-4 hover:shadow-md transition-shadow relative overflow-hidden group">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-secondary"></div>
<div class="flex-1">
<div class="flex justify-between items-start mb-2">
<span class="px-2 py-0.5 bg-secondary-fixed text-on-secondary-fixed-variant rounded text-label-xs font-bold uppercase tracking-wider">옥션 마감</span>
<span class="text-label-xs font-label-md text-on-surface-variant flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">schedule</span>
09:00 - 10:00
</span>
</div>
<h4 class="font-title-sm text-title-sm text-on-surface mb-1">1홀 전기공사 옥션 마감</h4>
<div class="flex items-center gap-1 text-label-md font-label-md text-on-surface-variant">
<span class="material-symbols-outlined text-[16px]">location_on</span>
KINTEX 1전시장 사무동
</div>
<div class="mt-4 pt-4 border-t border-outline-variant flex items-center justify-between">
<div class="flex -space-x-2">
<img class="w-6 h-6 rounded-full border-2 border-white object-cover" data-alt="A small avatar of a professional business woman with short hair and an optimistic look, wearing a light blue blouse. Studio lighting." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBJHwhGIOON8yXhp3OwDV5N9YwutvIhEUjKAgdEQICRDf_Xqri2ZL5WOQeG9sNzzVHymgyj3NuXaLaXK3tLXZy4MfyUvXdBlCokrU0A-i-VQ5WRuIZXoxb4ORndnvvzir3ityxxIMR4BfveRgnRKUf5GFw7or6g-_klVywfxj_S5biFpgglxnxD7zM9zQKzsc8AinL_NCrxe_Fi9I8jGcUkgSKSDfllPaEiGniBIFwmhh-0O1pSCbslzjL8ahu5w1-GhjX8pXRTBkY"/>
<img class="w-6 h-6 rounded-full border-2 border-white object-cover" data-alt="A small avatar of a young male professional in creative business casual attire, dark glasses, warm indoor lighting." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCwqFmeEJ9_FpTvEn5lVQkr9GeAa8bmo7lgARBRUHn0njL4JPRUjiaJpZMIH7Jut1C1N5LU20er0AWp-6GGAlH-GqZbcusLBDcuZrAhJtxDjGW2PKX5AQ9pTXlmZ3Y_WxKve6qSUkajhRxQZBoOCYlG157Qf_P_XtuT3lf9rJ2iLvJ0JkygxQiakGFboeiVlId5oe8MjLy3jxe9nhZFrgrpQwAB5hYF3wHOzCKJ8tW1nlyAI4QeNyblU6XUS3GpGn9uymtUxKZwkv4"/>
<div class="w-6 h-6 rounded-full border-2 border-white bg-surface-container text-[8px] flex items-center justify-center text-on-surface-variant font-bold">+3</div>
</div>
<button class="text-primary opacity-0 group-hover:opacity-100 transition-opacity"><span class="material-symbols-outlined">more_horiz</span></button>
</div>
</div>
</div>
<!-- Agenda Card 2 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-stack-md flex gap-4 hover:shadow-md transition-shadow relative overflow-hidden group">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-tertiary-container"></div>
<div class="flex-1">
<div class="flex justify-between items-start mb-2">
<span class="px-2 py-0.5 bg-tertiary-fixed text-on-tertiary-fixed-variant rounded text-label-xs font-bold uppercase tracking-wider">반입 슬롯</span>
<span class="text-label-xs font-label-md text-on-surface-variant flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">schedule</span>
14:00 - 16:00
</span>
</div>
<h4 class="font-title-sm text-title-sm text-on-surface mb-1">(주)테크솔루션 반입 작업</h4>
<div class="flex items-center gap-1 text-label-md font-label-md text-on-surface-variant">
<span class="material-symbols-outlined text-[16px]">location_on</span>
2홀 B구역 하역장
</div>
</div>
</div>
<!-- Agenda Card 3 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-stack-md flex gap-4 hover:shadow-md transition-shadow relative overflow-hidden group">
<div class="absolute left-0 top-0 bottom-0 w-1 bg-primary"></div>
<div class="flex-1">
<div class="flex justify-between items-start mb-2">
<span class="px-2 py-0.5 bg-primary-fixed text-on-primary-fixed-variant rounded text-label-xs font-bold uppercase tracking-wider">행사 점검</span>
<span class="text-label-xs font-label-md text-on-surface-variant flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">schedule</span>
16:30 - 17:30
</span>
</div>
<h4 class="font-title-sm text-title-sm text-on-surface mb-1">스마트팩토리 현장 안전 점검</h4>
<div class="flex items-center gap-1 text-label-md font-label-md text-on-surface-variant">
<span class="material-symbols-outlined text-[16px]">location_on</span>
1~3홀 전구역
</div>
</div>
</div>
<!-- Stats/Briefing -->
<div class="mt-8 p-6 rounded-2xl bg-gradient-to-br from-primary to-primary-container text-on-primary shadow-lg">
<h5 class="font-label-md text-label-md text-on-primary-container mb-4">오늘의 전시 브리핑</h5>
<div class="grid grid-cols-2 gap-4">
<div class="bg-white/10 backdrop-blur-md p-3 rounded-lg border border-white/20">
<p class="text-label-xs mb-1 opacity-80">누적 참관객</p>
<p class="font-headline-md text-headline-md">12.4k</p>
</div>
<div class="bg-white/10 backdrop-blur-md p-3 rounded-lg border border-white/20">
<p class="text-label-xs mb-1 opacity-80">점검 완료율</p>
<p class="font-headline-md text-headline-md">86%</p>
</div>
</div>
<button class="w-full mt-4 py-2 border border-white/30 rounded-lg text-label-md font-bold hover:bg-white/10 transition-colors">전체 통계 보기</button>
</div>
</div>
</aside>
</main>
<!-- Interactions Layer -->
<div class="fixed bottom-8 right-8 z-[100] pointer-events-none" id="toast-container"></div>
<script>
// Simple Interaction logic
document.querySelectorAll('.calendar-cell').forEach(cell => {
cell.addEventListener('click', () => {
// Mock behavior: transition active day
document.querySelectorAll('.calendar-cell').forEach(c => c.classList.remove('bg-primary-fixed/30', 'ring-2', 'ring-primary', 'ring-inset'));
cell.classList.add('bg-primary-fixed/30', 'ring-2', 'ring-primary', 'ring-inset');
// Show notification for UX feedback
const day = cell.querySelector('span').innerText;
showToast(`2026년 8월 ${day}일 일정을 불러왔습니다.`);
});
});
function showToast(message) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'pointer-events-auto bg-inverse-surface text-inverse-on-surface px-6 py-3 rounded-full shadow-2xl flex items-center gap-3 mb-3 animate-bounce-short';
toast.innerHTML = `
<span class="material-symbols-outlined text-tertiary-fixed">check_circle</span>
<span class="text-label-md">${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('opacity-0', 'translate-y-4');
toast.style.transition = 'all 0.5s ease';
setTimeout(() => toast.remove(), 500);
}, 3000);
}
</script>
<style>
@keyframes bounce-short {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.animate-bounce-short {
animation: bounce-short 0.4s ease-out;
}
</style>
</body></html>

View File

@ -1,363 +0,0 @@
<!DOCTYPE html>
<html lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>쪽지 - KINTEX AI 전시관리</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&amp;family=Geist:wght@500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed-dim": "#72daa9",
"surface-dim": "#d3d9f1",
"on-error": "#ffffff",
"secondary-fixed": "#e5deff",
"on-tertiary-fixed-variant": "#005236",
"surface-container-low": "#f2f3ff",
"surface-container": "#eaedff",
"inverse-primary": "#a2c9ff",
"surface-container-lowest": "#ffffff",
"on-tertiary-container": "#68d1a0",
"outline-variant": "#c2c7d1",
"on-primary": "#ffffff",
"on-secondary-container": "#f3edff",
"tertiary-fixed": "#8ef7c4",
"inverse-surface": "#293042",
"tertiary": "#003e28",
"inverse-on-surface": "#eef0ff",
"on-tertiary-fixed": "#002113",
"surface-tint": "#24609e",
"on-primary-fixed": "#001c38",
"primary-fixed": "#d3e4ff",
"surface-container-highest": "#dbe2fa",
"secondary": "#5427e6",
"error-red": "#ba1a1a",
"surface-variant": "#dbe2fa",
"on-secondary-fixed": "#1b0063",
"on-error-container": "#93000a",
"primary": "#003765",
"tertiary-container": "#00583b",
"on-secondary": "#ffffff",
"on-primary-container": "#91c0ff",
"secondary-fixed-dim": "#c9bfff",
"on-background": "#141b2c",
"surface": "#faf8ff",
"on-surface": "#141b2c",
"on-secondary-fixed-variant": "#4500d8",
"surface-bright": "#f9f9ff",
"error-container": "#ffdad6",
"secondary-container": "#6d4aff",
"background": "#faf8ff",
"on-primary-fixed-variant": "#004881",
"on-tertiary": "#ffffff",
"on-surface-variant": "#424750",
"surface-container-high": "#e1e7ff",
"primary-fixed-dim": "#a2c9ff",
"outline": "#727781",
"primary-container": "#004e8b",
"error": "#ba1a1a"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"base": "4px",
"stack-sm": "8px",
"section-padding": "96px",
"stack-lg": "24px",
"stack-md": "16px",
"gutter": "16px",
"container-padding": "24px"
},
"fontFamily": {
"display-lg": ["Inter"],
"body-sm": ["Inter"],
"title-sm": ["Inter"],
"label-xs": ["Geist"],
"label-md": ["Geist"],
"headline-md": ["Inter"],
"body-base": ["Inter"]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.message-active { border-left: 4px solid #003765; background-color: #f2f3ff; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #dbe2fa; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #c2c7d1; }
</style>
</head>
<body class="bg-surface-bright text-on-surface overflow-hidden h-screen flex flex-col">
<!-- Top AppBar -->
<header class="bg-primary dark:bg-primary-container text-on-primary dark:text-on-primary-container flex justify-between items-center w-full px-container-padding h-16 shadow-md z-30">
<div class="flex items-center gap-stack-md">
<span class="font-title-sm text-title-sm font-bold text-on-primary">KINTEX AI 전시관리</span>
</div>
<div class="flex items-center gap-stack-md">
<div class="relative flex items-center bg-primary-container/20 rounded-full px-4 py-1.5 border border-on-primary/20">
<span class="material-symbols-outlined text-[20px] mr-2">search</span>
<input class="bg-transparent border-none focus:ring-0 text-label-md font-label-md placeholder:text-on-primary-fixed-variant w-48" placeholder="통합 검색..." type="text"/>
</div>
<div class="flex gap-2">
<button class="p-2 hover:bg-primary-container/20 transition-colors rounded-full"><span class="material-symbols-outlined">notifications</span></button>
<button class="p-2 hover:bg-primary-container/20 transition-colors rounded-full"><span class="material-symbols-outlined">settings</span></button>
<button class="p-2 hover:bg-primary-container/20 transition-colors rounded-full"><span class="material-symbols-outlined">help</span></button>
</div>
<div class="h-8 w-8 rounded-full bg-surface-container-highest overflow-hidden border border-on-primary/20">
<img class="w-full h-full object-cover" data-alt="A professional headshot of a middle-aged Korean businesswoman with glasses and a friendly, confident expression, captured against a clean professional blue background in high definition photography style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuC5rKqg2BdLb7_vCTJoBxVXmCmPoytY9hx4E_7SfcwPO2bohMXp3cfqiRMWBp3sbHEKgECduIF3LvN6lIRUQ1cIYE6WKe76wmo5BcVv-DpUVCux-pEiDiLgPnmVoH6ddUj_Aa3hYCcCvLS9_Vk2BDz5ZoEIo5XTiiwtrwTxJgnhciOl0Y8kZ-4UoFANSJv8UdVreCXEwqgEMw8j9p248uQkRUvzhLSaeAMSTtRiO0eYZ4JWa1a1OtbJ4rhT8OaZOOQUwKMu0gBD32s"/>
</div>
</div>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- Left SideNavBar (Main Navigation) -->
<aside class="fixed left-0 top-16 bottom-0 w-[240px] flex flex-col p-stack-md gap-stack-sm bg-surface-container-low dark:bg-surface-container-lowest border-r border-outline-variant z-20">
<div class="mb-stack-lg px-2 pt-2">
<img alt="KINTEX 로고" class="h-8 w-auto" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCJR_RAHfgu28C4f_aqO2MiJrSmrLanG4w4MMSTRHAcRC-l_R4Oz0nwfcIdloJ9sSAy5JY5mcb3owuBgr4LSVFJs-2RvIvx1bQCR1mh7M5VSjgbwnuHwIBMazmY62BBjKlYmudQX2p0-3u3FHiF8TJttFpEDaoPsTk4jb2w_Y5gy-1Mme5i3okR6uN7uz6uNmgKDXnUDOaIC5DSWb2OnpcU53duc64TKp9leFYNeaFfUXjc9oak30BFMiJLhqK1aCGtHHncUyq_VQM"/>
<p class="mt-4 font-headline-md text-headline-md text-primary font-black">전시관리 시스템</p>
<p class="font-label-md text-label-md text-on-surface-variant">KINTEX AI Admin</p>
</div>
<nav class="flex flex-col gap-1">
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-variant/50 rounded-xl transition-all group active:scale-95 duration-100" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-label-md text-label-md">대시보드</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-variant/50 rounded-xl transition-all group active:scale-95 duration-100" href="#">
<span class="material-symbols-outlined">event</span>
<span class="font-label-md text-label-md">전시 일정</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container font-bold rounded-xl active:scale-95 duration-100" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">mail</span>
<span class="font-label-md text-label-md">쪽지</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-variant/50 rounded-xl transition-all group active:scale-95 duration-100" href="#">
<span class="material-symbols-outlined">task</span>
<span class="font-label-md text-label-md">업무 관리</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-variant/50 rounded-xl transition-all group active:scale-95 duration-100" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-md text-label-md">설정</span>
</a>
</nav>
<div class="mt-auto flex flex-col gap-1 border-t border-outline-variant pt-stack-md">
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-highest transition-all rounded-xl" href="#">
<span class="material-symbols-outlined">support_agent</span>
<span class="font-label-md text-label-md">지원센터</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-highest transition-all rounded-xl" href="#">
<span class="material-symbols-outlined">logout</span>
<span class="font-label-md text-label-md">로그아웃</span>
</a>
</div>
</aside>
<!-- Sub-Navigation Rail & Messaging Panes -->
<main class="ml-[240px] flex w-full h-full bg-surface-container-lowest overflow-hidden">
<!-- Folder List Pane -->
<div class="w-64 border-r border-outline-variant flex flex-col bg-surface-container-low/30 h-full">
<div class="p-4 border-b border-outline-variant">
<button class="w-full bg-primary text-on-primary py-3 px-4 rounded-full flex items-center justify-center gap-2 font-label-md text-label-md shadow-md hover:bg-primary-container transition-colors active:scale-95">
<span class="material-symbols-outlined text-[20px]">edit_note</span>
쪽지 쓰기
</button>
</div>
<div class="p-2 overflow-y-auto">
<div class="space-y-1">
<a class="flex items-center justify-between px-3 py-2.5 bg-primary-container/10 text-primary rounded-lg font-bold group" href="#">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[20px]">inbox</span>
<span class="text-body-base font-body-base">받은 쪽지함</span>
</div>
<span class="bg-primary text-on-primary text-[10px] px-2 py-0.5 rounded-full">12</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-surface-variant/40 rounded-lg group transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">send</span>
<span class="text-body-base font-body-base">보낸 쪽지함</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-surface-variant/40 rounded-lg group transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">draft</span>
<span class="text-body-base font-body-base">임시보관함</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-surface-variant/40 rounded-lg group transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">report</span>
<span class="text-body-base font-body-base">스팸 쪽지함</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-surface-variant/40 rounded-lg group transition-colors border-t border-outline-variant/30 mt-2" href="#">
<span class="material-symbols-outlined text-[20px]">delete</span>
<span class="text-body-base font-body-base">휴지통</span>
</a>
</div>
</div>
</div>
<!-- Middle Message List Pane -->
<div class="w-96 border-r border-outline-variant flex flex-col h-full bg-white">
<div class="p-4 flex flex-col gap-3 border-b border-outline-variant">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 material-symbols-outlined text-outline text-[18px]">search</span>
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm font-body-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none" placeholder="쪽지 검색" type="text"/>
</div>
<div class="flex items-center gap-2">
<button class="px-3 py-1 bg-primary-container/20 text-primary border border-primary-container/30 rounded-full font-label-md text-label-md">전체</button>
<button class="px-3 py-1 hover:bg-surface-container text-on-surface-variant border border-outline-variant rounded-full font-label-md text-label-md">안읽음</button>
<button class="px-3 py-1 hover:bg-surface-container text-on-surface-variant border border-outline-variant rounded-full font-label-md text-label-md">중요</button>
</div>
</div>
<div class="flex-1 overflow-y-auto divide-y divide-outline-variant/50">
<!-- Message Item Active -->
<div class="p-4 cursor-pointer hover:bg-surface-container-low transition-colors message-active relative group">
<div class="flex justify-between items-start mb-1">
<span class="font-bold text-on-surface text-body-base">박진우 본부장</span>
<span class="text-label-xs font-label-xs text-outline">오전 10:45</span>
</div>
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 bg-primary rounded-full"></span>
<h3 class="font-bold text-on-surface text-body-base truncate">2026 스마트팩토리 전시홀 배치 협의 건</h3>
</div>
<p class="text-body-sm font-body-sm text-on-surface-variant line-clamp-2">금일 오후 3시에 있을 전시홀 배치 관련하여 사전에 공유드린 도면...</p>
<div class="absolute right-4 bottom-4 opacity-0 group-hover:opacity-100 transition-opacity">
<span class="material-symbols-outlined text-outline hover:text-primary">star</span>
</div>
</div>
<!-- Message Item 2 -->
<div class="p-4 cursor-pointer hover:bg-surface-container-low transition-colors">
<div class="flex justify-between items-start mb-1">
<span class="text-on-surface text-body-base font-semibold">김서연 과장</span>
<span class="text-label-xs font-label-xs text-outline">어제</span>
</div>
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 bg-transparent"></span>
<h3 class="text-on-surface text-body-base truncate">차주 보안 업체 계약 연장 관련 건</h3>
</div>
<p class="text-body-sm font-body-sm text-on-surface-variant line-clamp-2">보안 업체에서 전달받은 계약 연장 조건입니다. 확인 부탁드립...</p>
</div>
<!-- Message Item 3 -->
<div class="p-4 cursor-pointer hover:bg-surface-container-low transition-colors">
<div class="flex justify-between items-start mb-1">
<span class="text-on-surface text-body-base font-semibold">정훈 팀장</span>
<span class="text-label-xs font-label-xs text-outline">10월 24일</span>
</div>
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 bg-transparent"></span>
<h3 class="text-on-surface text-body-base truncate">[공지] 제1전시장 환경 정비 안내</h3>
</div>
<p class="text-body-sm font-body-sm text-on-surface-variant line-clamp-2">안녕하세요. 이번 주말 전시장 전체 청소 및 방역이 실시될 예정입니다...</p>
</div>
<!-- Message Item 4 -->
<div class="p-4 cursor-pointer hover:bg-surface-container-low transition-colors">
<div class="flex justify-between items-start mb-1">
<span class="text-on-surface text-body-base font-semibold">이민재 대리</span>
<span class="text-label-xs font-label-xs text-outline">10월 23일</span>
</div>
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 bg-transparent"></span>
<h3 class="text-on-surface text-body-base truncate">VIP 케이터링 업체 제안서 송부</h3>
</div>
<p class="text-body-sm font-body-sm text-on-surface-variant line-clamp-2">차주 행사에 사용될 케이터링 업체 3곳의 제안서와 견적서 취합본입니...</p>
</div>
</div>
</div>
<!-- Right Reading Pane -->
<div class="flex-1 flex flex-col h-full bg-white relative">
<!-- Message Header -->
<div class="px-8 py-6 border-b border-outline-variant">
<div class="flex justify-between items-start mb-6">
<h1 class="text-headline-md font-headline-md text-on-surface">2026 스마트팩토리 전시홀 배치 협의 건</h1>
<div class="flex gap-2">
<button class="p-2 hover:bg-surface-container rounded-full text-on-surface-variant transition-colors"><span class="material-symbols-outlined">print</span></button>
<button class="p-2 hover:bg-surface-container rounded-full text-on-surface-variant transition-colors"><span class="material-symbols-outlined">delete</span></button>
<button class="p-2 hover:bg-surface-container rounded-full text-on-surface-variant transition-colors"><span class="material-symbols-outlined">more_vert</span></button>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<img alt="박진우 본부장" class="w-12 h-12 rounded-full border border-outline-variant object-cover" src="https://lh3.googleusercontent.com/aida/AP1WRLsXFsekmPa77JGx0JMCs31rJ8hCsvn7xGfnRM9YyXX7chbyO5TIB8FjZBm5fuFcHrQrYs35rIMDAjOdQ7L5aS6UTo4NYut5TRziZbF74ZvNWLE74dsJn7t1r9a7q3Ix39o7yZfIRaOTvJrCAHxHopNa-re1XpQvCTPmq8Lv42KQOHH6nzp1ze0UtP0LbreAHgyKKhKyKztA-HxR1EaCG0tF47e0ojeYQQySFKsAjShPLTVV1GHZvv3aQa8"/>
<div>
<div class="flex items-center gap-2">
<span class="font-bold text-on-surface text-body-base">박진우 본부장</span>
<span class="text-label-md font-label-md text-on-surface-variant bg-surface-container px-2 py-0.5 rounded">전시운영팀</span>
</div>
<p class="text-label-xs font-label-xs text-outline mt-1">2024년 10월 25일 오전 10:45 (받음)</p>
</div>
</div>
<button class="flex items-center gap-2 px-4 py-2 border border-outline rounded-lg font-label-md text-label-md text-on-surface-variant hover:bg-surface-container-low transition-colors">
<span class="material-symbols-outlined text-[18px]">reply</span>
답장
</button>
</div>
</div>
<!-- Message Body -->
<div class="flex-1 px-8 py-8 overflow-y-auto text-body-base font-body-base text-on-surface leading-relaxed">
<p class="mb-4">안녕하세요, 운영팀의 박진우입니다.</p>
<p class="mb-4">내년 개최 예정인 '2026 스마트팩토리 엑스포'의 핵심 전시홀 배치와 관련하여 주요 부스 레이아웃 초안을 전달 드립니다.</p>
<p class="mb-4">이번 전시회는 기존보다 참가 기업 수가 15% 가량 증가할 것으로 예상되어, 관람객의 동선 혼잡을 방지하고 주요 글로벌 스폰서 업체들의 전시 공간을 전략적으로 배치하는 것이 관건입니다.</p>
<div class="bg-surface-container-low border-l-4 border-primary p-4 mb-4 rounded-r-lg">
<p class="font-bold mb-2">검토 요청 사항:</p>
<ul class="list-disc pl-5 space-y-1">
<li>제1전시장 A-E 구역 대형 부스 전력 공급 용량 확인</li>
<li>메인 로비 입구 측 네트워킹 라운지 배치 최적화</li>
<li>비상 탈출구 주변 여유 공간(3m 이상) 확보 여부</li>
</ul>
</div>
<p class="mb-4">첨부된 CAD 도면 파일을 확인해 주시고, 금일 오후 3시 회의 전까지 피드백 주시면 감사하겠습니다.</p>
<div class="mt-8 flex gap-4 p-4 border border-outline-variant rounded-xl bg-surface-bright">
<div class="w-12 h-12 bg-primary/10 rounded flex items-center justify-center text-primary">
<span class="material-symbols-outlined">description</span>
</div>
<div class="flex-1">
<p class="font-bold text-body-sm">2026_SmartFactory_Layout_V1.pdf</p>
<p class="text-label-xs text-outline">2.4 MB</p>
</div>
<button class="text-primary hover:underline font-label-md text-label-md">다운로드</button>
</div>
<p class="mt-8">감사합니다.<br/>박진우 드림</p>
</div>
<!-- Message Footer / Reply Area -->
<div class="p-6 border-t border-outline-variant bg-surface-container-lowest shadow-[0_-4px_10px_rgba(0,0,0,0.03)]">
<div class="border border-outline-variant rounded-xl overflow-hidden focus-within:border-primary transition-all">
<textarea class="w-full border-none focus:ring-0 p-4 text-body-base min-h-[100px] resize-none" placeholder="빠른 답장을 입력하세요..."></textarea>
<div class="bg-surface-container-low px-4 py-2 flex justify-between items-center">
<div class="flex gap-2">
<button class="p-1.5 hover:bg-surface-variant rounded text-on-surface-variant transition-colors"><span class="material-symbols-outlined text-[20px]">attach_file</span></button>
<button class="p-1.5 hover:bg-surface-variant rounded text-on-surface-variant transition-colors"><span class="material-symbols-outlined text-[20px]">image</span></button>
<button class="p-1.5 hover:bg-surface-variant rounded text-on-surface-variant transition-colors"><span class="material-symbols-outlined text-[20px]">mood</span></button>
</div>
<button class="bg-primary text-on-primary px-6 py-2 rounded-lg font-bold text-label-md shadow-sm hover:shadow-md transition-all active:scale-95">답장 보내기</button>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Floating Atmosphere Elements -->
<div class="fixed bottom-6 right-6 z-50">
<button class="w-14 h-14 bg-secondary text-on-secondary rounded-full flex items-center justify-center shadow-xl hover:scale-110 active:scale-90 transition-all">
<span class="material-symbols-outlined text-[28px]">support_agent</span>
</button>
</div>
<script>
// Simple micro-interaction for marking messages as read or changing state
document.querySelectorAll('.cursor-pointer').forEach(item => {
item.addEventListener('click', function() {
// Remove active class from all
document.querySelectorAll('.cursor-pointer').forEach(el => el.classList.remove('message-active'));
// Add to clicked
this.classList.add('message-active');
});
});
</script>
</body></html>

View File

@ -1,431 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - 공지사항</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"background": "#faf8ff",
"outline-variant": "#c2c7d1",
"surface": "#faf8ff",
"on-tertiary-container": "#68d1a0",
"secondary-container": "#6d4aff",
"primary-fixed": "#d3e4ff",
"surface-container-high": "#e1e7ff",
"surface-container-low": "#f2f3ff",
"on-background": "#141b2c",
"error-container": "#ffdad6",
"primary-container": "#004e8b",
"surface-container": "#eaedff",
"on-error-container": "#93000a",
"on-surface-variant": "#424750",
"on-secondary": "#ffffff",
"tertiary-fixed-dim": "#72daa9",
"on-tertiary": "#ffffff",
"on-primary-fixed-variant": "#004881",
"secondary-fixed": "#e5deff",
"on-secondary-fixed-variant": "#4500d8",
"surface-container-lowest": "#ffffff",
"surface-dim": "#d3d9f1",
"primary": "#003765", /* Adjusted to KINTEX Blue #0066B3 context via primary scale */
"kintex-blue": "#0066B3",
"surface-variant": "#dbe2fa",
"tertiary-fixed": "#8ef7c4",
"on-secondary-fixed": "#1b0063",
"on-primary-fixed": "#001c38",
"on-secondary-container": "#f3edff",
"surface-tint": "#24609e",
"on-error": "#ffffff",
"surface-bright": "#faf8ff",
"error": "#ba1a1a",
"on-tertiary-fixed-variant": "#005236",
"on-primary": "#ffffff",
"tertiary-container": "#00583b",
"inverse-surface": "#293042",
"error-red": "#ba1a1a",
"surface-container-highest": "#dbe2fa",
"secondary": "#5427e6",
"on-primary-container": "#91c0ff",
"tertiary": "#003e28",
"secondary-fixed-dim": "#c9bfff",
"on-tertiary-fixed": "#002113",
"primary-fixed-dim": "#a2c9ff",
"inverse-primary": "#a2c9ff",
"outline": "#727781",
"on-surface": "#141b2c",
"inverse-on-surface": "#eef0ff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "24px",
"container-padding": "24px",
"section-padding": "96px",
"gutter": "16px",
"stack-sm": "8px",
"stack-md": "16px",
"base": "4px"
},
"fontFamily": {
"title-sm": ["Inter"],
"body-sm": ["Inter"],
"label-md": ["Geist"],
"display-lg": ["Inter"],
"headline-md": ["Inter"],
"body-base": ["Inter"],
"label-xs": ["Geist"]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.line-icon-style {
font-variation-settings: 'wght' 300, 'opsz' 20;
}
.zebra-table tr:nth-child(even) {
background-color: #f8fafc;
}
.notice-row:hover {
background-color: #f1f5f9 !important;
cursor: pointer;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
</style>
</head>
<body class="bg-background text-on-background font-body-base overflow-hidden">
<!-- Shell Layout: SideNav -->
<aside class="fixed left-0 top-0 bottom-0 flex flex-col z-40 w-64 bg-primary text-on-primary shadow-lg">
<div class="p-6 flex flex-col gap-1">
<h1 class="text-title-sm font-title-sm font-bold text-on-primary">KINTEX AI</h1>
<p class="text-label-md font-label-md opacity-80">전시관리 시스템</p>
</div>
<nav class="flex-1 mt-4 overflow-y-auto px-2">
<div class="space-y-1">
<!-- Dashboard -->
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors active:scale-95 duration-150" href="#">
<span class="material-symbols-outlined line-icon-style">dashboard</span>
<span class="text-label-md font-label-md">대시보드</span>
</a>
<!-- Notice Board - ACTIVE -->
<a class="bg-primary-container text-on-primary-container rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 active:scale-95 transition-all duration-150 shadow-sm" href="#">
<span class="material-symbols-outlined line-icon-style">campaign</span>
<span class="text-label-md font-label-md">공지사항</span>
</a>
<!-- Exhibition Management -->
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors active:scale-95 duration-150" href="#">
<span class="material-symbols-outlined line-icon-style">event</span>
<span class="text-label-md font-label-md">전시 관리</span>
</a>
<!-- User Management -->
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors active:scale-95 duration-150" href="#">
<span class="material-symbols-outlined line-icon-style">group</span>
<span class="text-label-md font-label-md">사용자 관리</span>
</a>
<!-- Device Monitoring -->
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors active:scale-95 duration-150" href="#">
<span class="material-symbols-outlined line-icon-style">router</span>
<span class="text-label-md font-label-md">기기 모니터링</span>
</a>
<!-- Statistics -->
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors active:scale-95 duration-150" href="#">
<span class="material-symbols-outlined line-icon-style">analytics</span>
<span class="text-label-md font-label-md">통계 분석</span>
</a>
</div>
</nav>
<div class="p-4 border-t border-on-primary/10">
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined line-icon-style">settings</span>
<span class="text-label-md font-label-md">설정</span>
</a>
<a class="text-on-primary/70 hover:text-on-primary hover:bg-on-primary/10 rounded-lg mx-2 my-1 flex items-center gap-3 px-4 py-3 transition-colors" href="#">
<span class="material-symbols-outlined line-icon-style">logout</span>
<span class="text-label-md font-label-md">로그아웃</span>
</a>
</div>
</aside>
<!-- Shell Layout: TopAppBar -->
<header class="fixed top-0 right-0 left-64 h-16 flex justify-between items-center px-6 z-30 bg-surface-bright border-b border-outline-variant">
<div class="flex items-center gap-4">
<span class="text-primary font-bold text-headline-md tracking-tight">KINTEX AI 전시관리</span>
<div class="h-4 w-px bg-outline-variant mx-2"></div>
<nav class="flex gap-4 text-label-md font-label-md text-on-surface-variant">
<span class="text-primary font-bold">공지사항</span>
<span class="hover:text-primary cursor-pointer transition-colors">시스템 공지</span>
<span class="hover:text-primary cursor-pointer transition-colors">운영 매뉴얼</span>
</nav>
</div>
<div class="flex items-center gap-4">
<div class="relative group">
<span class="material-symbols-outlined text-on-surface-variant p-2 rounded-full hover:bg-surface-container cursor-pointer transition-all">notifications</span>
<span class="absolute top-1.5 right-1.5 w-2 h-2 bg-error rounded-full border border-surface-bright"></span>
</div>
<div class="flex items-center gap-3 pl-4 border-l border-outline-variant">
<div class="text-right">
<p class="text-label-md font-label-md text-on-surface">김관리 팀장</p>
<p class="text-label-xs font-label-xs text-on-surface-variant opacity-70">시스템 관리자</p>
</div>
<img class="w-10 h-10 rounded-full border border-outline-variant object-cover" data-alt="A professional close-up studio portrait of a corporate administrator in a modern office environment. The person is wearing a clean, dark blue business suit, looking directly at the camera with a friendly and professional expression. The background is a soft, blurred office interior with high-end glass partitions and soft warm lighting that complements the clean-tech aesthetic of the UI. Professional lighting enhances facial features for a high-quality ID photo look." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBCwgHgvXoIPTXdvHcKYEuVvZzyTlo9T2gHNmT0t5TWrCgL5CcjgfaNEyL7yche1I2KzpojxaOnEqrlZBN__AIsjKXdXyaL-Csq33S2UiM3K-0G8vh9G546xErV2yjjlvUAdeFHCVOUq9KvaNDMdW5HFPs_6LXLEnnDswWa7fm0FygwICMSrcjZXbqN6p0bVx2EBNq2Qbf7FW8WeqSLdxBrmSUykRwldbchl93pY9aaFIjpz_131bSXf30hTknmSvjXUfz8VULNffY"/>
</div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="ml-64 pt-16 h-screen overflow-y-auto">
<div class="max-w-[1280px] mx-auto p-12 space-y-8">
<!-- Page Header Section -->
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div>
<h2 class="text-display-lg font-display-lg text-primary">공지사항</h2>
<p class="text-body-base text-on-surface-variant mt-2">KINTEX AI 전시관리 시스템의 새로운 소식과 주요 안내사항을 확인하세요.</p>
</div>
<div class="flex gap-3">
<div class="relative min-w-[320px]">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant">search</span>
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-base focus:ring-2 focus:ring-kintex-blue/20 focus:border-kintex-blue transition-all outline-none" placeholder="공지 제목 또는 내용 검색" type="text"/>
</div>
<button class="bg-kintex-blue text-on-primary px-6 py-2 rounded-full font-label-md text-label-md hover:bg-primary transition-all shadow-sm flex items-center gap-2 active:scale-95">
<span class="material-symbols-outlined text-[18px]">add</span>
공지 작성
</button>
</div>
</div>
<!-- Pinned Notice Section -->
<div class="relative overflow-hidden rounded-xl border-l-4 border-error bg-surface-container-lowest shadow-md p-6 hover:shadow-lg transition-shadow cursor-pointer group">
<div class="flex items-start justify-between">
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<span class="bg-error text-white text-label-xs font-label-xs px-2 py-0.5 rounded uppercase tracking-wider">중요</span>
<span class="text-label-md font-label-md text-on-surface-variant">운영 가이드라인</span>
</div>
<h3 class="text-title-sm font-title-sm text-on-surface group-hover:text-kintex-blue transition-colors">
중요: 2026 스마트팩토리 엑스포 운영 규정 변경 안내
</h3>
<p class="text-body-sm text-on-surface-variant line-clamp-1 opacity-80">
내년도 개최 예정인 스마트팩토리 엑스포의 전시관 배치 및 안전 관리 규정이 강화됨에 따라 주요 변경 사항을 공지합니다. 전시 업체 및 관리자 분들께서는 반드시 숙지 부탁드립니다.
</p>
</div>
<div class="text-right whitespace-nowrap pl-8">
<p class="text-label-md font-label-md text-on-surface">관리본부</p>
<p class="text-label-xs font-label-xs text-on-surface-variant mt-1">2023.10.27</p>
</div>
</div>
<!-- Glassy Highlight Decorative -->
<div class="absolute -right-8 -bottom-8 w-24 h-24 bg-error/5 rounded-full blur-2xl"></div>
</div>
<!-- Notice List Table Container -->
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden shadow-sm">
<table class="w-full text-left border-collapse zebra-table">
<thead>
<tr class="bg-surface-container text-on-surface-variant text-label-md font-label-md uppercase tracking-wider">
<th class="px-6 py-4 w-16 text-center">번호</th>
<th class="px-6 py-4">제목</th>
<th class="px-6 py-4 w-32">작성자</th>
<th class="px-6 py-4 w-32">게시일</th>
<th class="px-6 py-4 w-24 text-center">조회수</th>
</tr>
</thead>
<tbody class="text-body-base">
<!-- Row 1 -->
<tr class="notice-row border-b border-outline-variant/30" onclick="openNoticeModal()">
<td class="px-6 py-5 text-center text-on-surface-variant">42</td>
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<span class="text-on-surface font-medium">제 2전시장 AI 통합 관제 시스템 점검 공지</span>
<span class="material-symbols-outlined text-[16px] text-outline">attach_file</span>
</div>
</td>
<td class="px-6 py-5 text-on-surface-variant">시스템운영팀</td>
<td class="px-6 py-5 text-on-surface-variant">2023.10.25</td>
<td class="px-6 py-5 text-center text-on-surface-variant">1,204</td>
</tr>
<!-- Row 2 -->
<tr class="notice-row border-b border-outline-variant/30">
<td class="px-6 py-5 text-center text-on-surface-variant">41</td>
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<span class="text-on-surface">KINTEX 멤버십 서비스 혜택 변경 안내 (11월부터 적용)</span>
</div>
</td>
<td class="px-6 py-5 text-on-surface-variant">전략마케팅팀</td>
<td class="px-6 py-5 text-on-surface-variant">2023.10.22</td>
<td class="px-6 py-5 text-center text-on-surface-variant">856</td>
</tr>
<!-- Row 3 -->
<tr class="notice-row border-b border-outline-variant/30">
<td class="px-6 py-5 text-center text-on-surface-variant">40</td>
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<span class="text-on-surface">전시 참관객 데이터 분석 리포트 업데이트 완료 (Q3)</span>
<span class="material-symbols-outlined text-[16px] text-outline">attach_file</span>
</div>
</td>
<td class="px-6 py-5 text-on-surface-variant">데이터분석실</td>
<td class="px-6 py-5 text-on-surface-variant">2023.10.18</td>
<td class="px-6 py-5 text-center text-on-surface-variant">2,311</td>
</tr>
<!-- Row 4 -->
<tr class="notice-row border-b border-outline-variant/30">
<td class="px-6 py-5 text-center text-on-surface-variant">39</td>
<td class="px-6 py-5 text-on-surface">하반기 신규 전시 제안서 접수 기간 연장 알림</td>
<td class="px-6 py-5 text-on-surface-variant">기획사업본부</td>
<td class="px-6 py-5 text-on-surface-variant">2023.10.15</td>
<td class="px-6 py-5 text-center text-on-surface-variant">542</td>
</tr>
<!-- Row 5 -->
<tr class="notice-row border-b border-outline-variant/30">
<td class="px-6 py-5 text-center text-on-surface-variant">38</td>
<td class="px-6 py-5 text-on-surface">추석 연휴 기간 시스템 고객지원 센터 운영 시간 안내</td>
<td class="px-6 py-5 text-on-surface-variant">운영본부</td>
<td class="px-6 py-5 text-on-surface-variant">2023.09.25</td>
<td class="px-6 py-5 text-center text-on-surface-variant">1,098</td>
</tr>
</tbody>
</table>
<!-- Pagination -->
<div class="px-6 py-4 bg-surface-container-lowest flex justify-between items-center border-t border-outline-variant/30">
<p class="text-label-md font-label-md text-on-surface-variant">전체 1,242개의 항목 중 1-5 표시</p>
<div class="flex gap-1">
<button class="p-2 rounded hover:bg-surface-container-low text-on-surface-variant"><span class="material-symbols-outlined">chevron_left</span></button>
<button class="w-8 h-8 rounded flex items-center justify-center bg-kintex-blue text-white font-label-md text-label-md">1</button>
<button class="w-8 h-8 rounded flex items-center justify-center hover:bg-surface-container-low text-on-surface-variant font-label-md text-label-md">2</button>
<button class="w-8 h-8 rounded flex items-center justify-center hover:bg-surface-container-low text-on-surface-variant font-label-md text-label-md">3</button>
<button class="w-8 h-8 rounded flex items-center justify-center hover:bg-surface-container-low text-on-surface-variant font-label-md text-label-md">4</button>
<button class="w-8 h-8 rounded flex items-center justify-center hover:bg-surface-container-low text-on-surface-variant font-label-md text-label-md">5</button>
<button class="p-2 rounded hover:bg-surface-container-low text-on-surface-variant"><span class="material-symbols-outlined">chevron_right</span></button>
</div>
</div>
</div>
</div>
</main>
<!-- Detail View Modal (Glassmorphism Overlay) -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm hidden opacity-0 transition-opacity duration-300" id="noticeModal">
<div class="bg-surface-container-lowest w-full max-w-3xl max-h-[921px] rounded-2xl shadow-2xl overflow-hidden flex flex-col transform scale-95 transition-transform duration-300">
<!-- Modal Header -->
<div class="px-8 py-6 border-b border-outline-variant flex justify-between items-start">
<div class="space-y-2">
<div class="flex items-center gap-3">
<span class="bg-primary-fixed text-on-primary-fixed-variant text-label-xs font-label-xs px-2 py-0.5 rounded">점검안내</span>
<span class="text-label-md font-label-md text-on-surface-variant">No. 42</span>
</div>
<h3 class="text-headline-md font-headline-md text-on-surface">제 2전시장 AI 통합 관제 시스템 점검 공지</h3>
</div>
<button class="p-2 hover:bg-surface-container rounded-full transition-colors" onclick="closeNoticeModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<!-- Modal Meta Info -->
<div class="px-8 py-4 bg-surface-container-low flex gap-8">
<div class="flex items-center gap-2">
<span class="text-label-xs font-label-xs text-on-surface-variant uppercase opacity-60">작성자</span>
<span class="text-label-md font-label-md text-on-surface">시스템운영팀</span>
</div>
<div class="flex items-center gap-2 border-l border-outline-variant pl-8">
<span class="text-label-xs font-label-xs text-on-surface-variant uppercase opacity-60">등록일</span>
<span class="text-label-md font-label-md text-on-surface">2023.10.25 14:30</span>
</div>
<div class="flex items-center gap-2 border-l border-outline-variant pl-8">
<span class="text-label-xs font-label-xs text-on-surface-variant uppercase opacity-60">조회수</span>
<span class="text-label-md font-label-md text-on-surface">1,204</span>
</div>
</div>
<!-- Modal Body Content -->
<div class="px-8 py-8 flex-1 overflow-y-auto space-y-6">
<div class="text-body-base text-on-surface space-y-4 leading-relaxed">
<p>안녕하세요. KINTEX AI 전시관리 시스템 운영팀입니다.</p>
<p>시스템 안정성 강화 및 안정적인 서비스 제공을 위해 아래와 같이 제 2전시장 AI 통합 관제 시스템의 정기 점검이 진행될 예정입니다. 점검 시간 동안 해당 지역의 실시간 모니터링 기능이 일시적으로 제한될 수 있으니 운영에 참고하여 주시기 바랍니다.</p>
<div class="bg-surface-container-low p-5 rounded-lg border border-outline-variant/50 space-y-2">
<p class="font-bold">1. 점검 일시</p>
<p>• 2023년 11월 01일 (수) 02:00 ~ 05:00 (약 3시간)</p>
<p class="font-bold mt-4">2. 점검 대상</p>
<p>• 제 2전시장 전 구역 AI 카메라 및 데이터 허브</p>
<p class="font-bold mt-4">3. 점검 영향</p>
<p>• 점검 중 실시간 군중 밀집도 데이터 업데이트 지연</p>
<p>• 시스템 대시보드 내 특정 위젯 로딩 불가</p>
</div>
<p>이와 관련하여 문의사항이 있으시면 시스템운영팀(내선 104)으로 연락 주시기 바랍니다. 더 나은 서비스를 제공하기 위해 노력하겠습니다. 감사합니다.</p>
</div>
<!-- Attachments Section -->
<div class="pt-6 border-t border-outline-variant">
<h4 class="text-label-md font-label-md text-on-surface mb-3 flex items-center gap-2">
<span class="material-symbols-outlined text-[20px]">attach_file</span>
첨부파일 (1)
</h4>
<div class="flex flex-wrap gap-2">
<div class="flex items-center gap-3 px-4 py-3 bg-surface-container-low border border-outline-variant rounded-lg hover:border-kintex-blue hover:bg-white cursor-pointer transition-all group">
<span class="material-symbols-outlined text-kintex-blue">picture_as_pdf</span>
<div>
<p class="text-label-md font-label-md text-on-surface">점검 상세 매뉴얼.pdf</p>
<p class="text-label-xs font-label-xs text-on-surface-variant opacity-60">2.4 MB</p>
</div>
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-kintex-blue opacity-0 group-hover:opacity-100 transition-opacity ml-4">download</span>
</div>
</div>
</div>
</div>
<!-- Modal Footer -->
<div class="px-8 py-5 border-t border-outline-variant flex justify-end gap-3 bg-surface-container-lowest">
<button class="px-6 py-2 border border-outline-variant text-on-surface-variant rounded-lg font-label-md hover:bg-surface-container transition-all" onclick="closeNoticeModal()">닫기</button>
<button class="px-6 py-2 bg-kintex-blue text-white rounded-lg font-label-md hover:bg-primary transition-all shadow-sm">수정하기</button>
</div>
</div>
</div>
<script>
function openNoticeModal() {
const modal = document.getElementById('noticeModal');
const inner = modal.querySelector('div');
modal.classList.remove('hidden');
setTimeout(() => {
modal.classList.remove('opacity-0');
inner.classList.remove('scale-95');
inner.classList.add('scale-100');
}, 10);
}
function closeNoticeModal() {
const modal = document.getElementById('noticeModal');
const inner = modal.querySelector('div');
modal.classList.add('opacity-0');
inner.classList.add('scale-95');
inner.classList.remove('scale-100');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
// Close modal on escape key
document.addEventListener('keydown', function(event) {
if (event.key === "Escape") {
closeNoticeModal();
}
});
// Close modal on backdrop click
document.getElementById('noticeModal').addEventListener('click', function(event) {
if (event.target === this) {
closeNoticeModal();
}
});
</script>
</body></html>

View File

@ -1,481 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>의견접수 | KINTEX AI 전시관리 시스템</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind Configuration -->
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-secondary-container": "#f3edff",
"surface": "#faf8ff",
"secondary-fixed-dim": "#c9bfff",
"primary-fixed-dim": "#a2c9ff",
"on-secondary-fixed": "#1b0063",
"surface-variant": "#dbe2fa",
"secondary-container": "#6d4aff",
"secondary": "#5427e6",
"on-tertiary-fixed": "#002113",
"tertiary-fixed-dim": "#72daa9",
"surface-bright": "#faf8ff",
"tertiary-fixed": "#8ef7c4",
"outline": "#727781",
"surface-container-highest": "#dbe2fa",
"surface-tint": "#24609e",
"primary": "#003765",
"secondary-fixed": "#e5deff",
"background": "#faf8ff",
"surface-dim": "#d3d9f1",
"tertiary": "#003e28",
"on-primary": "#ffffff",
"outline-variant": "#c1c7d3",
"surface-container-low": "#f2f3ff",
"on-primary-fixed-variant": "#004881",
"on-primary-container": "#91c0ff",
"surface-container-lowest": "#ffffff",
"inverse-primary": "#a2c9ff",
"primary-container": "#004e8b",
"primary-fixed": "#d3e4ff",
"on-surface-variant": "#424750",
"surface-container-high": "#e1e7ff",
"on-error-container": "#93000a",
"tertiary-container": "#00583b",
"on-surface": "#141b2c",
"surface-container": "#eaedff",
"on-tertiary-container": "#68d1a0",
"inverse-on-surface": "#eef0ff",
"on-error": "#ffffff",
"on-tertiary": "#ffffff",
"on-background": "#141b2c",
"error": "#ba1a1a",
"error-container": "#ffdad6",
"on-secondary": "#ffffff",
"on-tertiary-fixed-variant": "#005236",
"on-secondary-fixed-variant": "#4500d8",
"inverse-surface": "#293042",
"on-primary-fixed": "#001c38",
"error-red": "#ba1a1a"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"section-padding": "96px",
"stack-lg": "24px",
"stack-md": "16px",
"container-padding": "24px",
"gutter": "16px",
"stack-sm": "8px",
"base": "4px"
},
"fontFamily": {
"body-base": ["Inter"],
"title-sm": ["Inter"],
"body-sm": ["Inter"],
"label-md": ["Geist"],
"display-lg": ["Inter"],
"label-xs": ["Geist"],
"headline-md": ["Inter"],
"display-lg-mobile": ["Inter"]
},
"fontSize": {
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body {
background-color: #faf8ff;
color: #141b2c;
}
.bento-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 24px;
}
.form-card {
background: #ffffff;
border: 1px solid #c1c7d3;
border-left: 4px solid #003765;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.form-card:hover {
box-shadow: 0 4px 20px rgba(0, 55, 101, 0.08);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 10px;
}
.ai-response-blob {
background: linear-gradient(135deg, #f2f3ff 0%, #eef0ff 100%);
border: 1px solid #dbe2fa;
position: relative;
}
.ai-response-blob::before {
content: 'AI-Powered';
position: absolute;
top: -10px;
right: 16px;
background: #6d4aff;
color: white;
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
</style>
</head>
<body class="font-body-base overflow-x-hidden">
<!-- SideNavBar -->
<aside class="bg-surface-container-low dark:bg-surface-container-low h-full w-64 fixed left-0 top-0 border-r border-outline-variant flex flex-col py-6 z-50">
<div class="px-6 mb-8">
<h1 class="font-display-lg text-display-lg font-bold text-primary dark:text-primary-fixed">KINTEX AI</h1>
<p class="text-on-surface-variant text-label-md mt-1">전시관리 시스템</p>
</div>
<nav class="flex-1 px-3 space-y-1 overflow-y-auto custom-scrollbar">
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="dashboard">dashboard</span>
<span>대시보드</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="event">event</span>
<span>전시 관리</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="group">group</span>
<span>사용자 관리</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="sensors">sensors</span>
<span>기기 모니터링</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="leaderboard">leaderboard</span>
<span>통계 분석</span>
</a>
<a class="flex items-center px-4 py-3 bg-secondary-container text-on-secondary-container font-bold border-l-4 border-secondary font-label-md rounded-r-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="rate_review">rate_review</span>
<span>의견접수</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="history">history</span>
<span>감사 로그</span>
</a>
</nav>
<div class="px-3 pt-4 border-t border-outline-variant mt-4">
<a class="flex items-center px-4 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="settings">settings</span>
<span>설정</span>
</a>
<a class="flex items-center px-4 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors font-label-md rounded-lg" href="#">
<span class="material-symbols-outlined mr-3" data-icon="help">help</span>
<span>도움말</span>
</a>
<a class="flex items-center px-4 py-2 text-error hover:bg-error-container transition-colors font-label-md rounded-lg mt-2" href="#">
<span class="material-symbols-outlined mr-3" data-icon="logout">logout</span>
<span>로그아웃</span>
</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-64 flex flex-col min-h-screen">
<!-- TopAppBar -->
<header class="bg-surface dark:bg-surface flex justify-between items-center px-container-padding h-16 border-b border-outline-variant sticky top-0 z-40">
<div class="flex items-center space-x-4">
<span class="font-headline-md text-headline-md font-bold text-on-surface">의견접수</span>
<div class="h-4 w-px bg-outline-variant"></div>
<span class="text-on-surface-variant font-body-sm">KINTEX AI 전시관리</span>
</div>
<div class="flex items-center space-x-6">
<div class="relative flex items-center bg-surface-container-low rounded-full px-4 py-2 border border-outline-variant">
<span class="material-symbols-outlined text-on-surface-variant mr-2" style="font-size: 20px;">search</span>
<input class="bg-transparent border-none focus:ring-0 text-sm w-48 font-body-sm" placeholder="검색어를 입력하세요..." type="text"/>
</div>
<div class="flex items-center space-x-3">
<button class="material-symbols-outlined p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-all" data-icon="notifications">notifications</button>
<button class="material-symbols-outlined p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-all" data-icon="settings">settings</button>
<div class="flex items-center space-x-2 pl-4 border-l border-outline-variant">
<div class="text-right">
<p class="text-label-md font-bold text-on-surface">관리자 01</p>
<p class="text-label-xs text-on-surface-variant">운영지원팀</p>
</div>
<img class="w-8 h-8 rounded-full border border-outline-variant" data-alt="A clean, professional headshot of a corporate administrator in a modern office environment, high-end photography with soft lighting and a blurred background featuring KINTEX architectural elements." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDXCm1COXIvBx7My6PQfiOj9ze0d3hyswLZwO0ZHMpYnzMbJOqkI5TC4RQASVDzqTigO11GC7W7kzqRlAqE79k71Q0G6yZTl9hg755dOcl5ZK01QOTjMU_QCyfLaUlGpwHFDRL0ocL67_OMV9K-gfYkjPKV-h3OPp_DtGKl7Zu0wyobzbtj0OliFk6M3wgLCf0sBz8AWcrotKBPdc8ct77-IRJ9HNR0I3z4F6vnCrZI2PBlQNoP4XjN_Gs4QqzRFouk8TZSJcArDuo"/>
</div>
</div>
</div>
</header>
<!-- Content Canvas -->
<div class="flex-1 p-8 max-w-[1280px] mx-auto w-full">
<!-- Header Section -->
<section class="mb-12">
<h2 class="font-display-lg text-display-lg text-primary mb-2">의견 및 제안 접수</h2>
<p class="text-on-surface-variant font-title-sm">시스템 개선 및 현장 운영 효율화를 위한 소중한 의견을 남겨주세요.</p>
</section>
<!-- Submission Form (Bento Style) -->
<div class="bento-grid mb-16">
<div class="col-span-12 lg:col-span-8">
<div class="form-card rounded-xl p-8 h-full">
<h3 class="font-title-sm text-primary mb-6 flex items-center">
<span class="material-symbols-outlined mr-2" data-icon="edit_note">edit_note</span>
의견 작성
</h3>
<form class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-label-md text-on-surface-variant mb-2">접수 카테고리</label>
<select class="w-full bg-surface-container-low border-outline-variant rounded-lg focus:border-primary focus:ring-primary text-body-base">
<option>카테고리를 선택하세요</option>
<option>규정 문의</option>
<option>개선 제안</option>
<option>불편 신고</option>
</select>
</div>
<div>
<label class="block text-label-md text-on-surface-variant mb-2">담당 부서</label>
<input class="w-full bg-surface-container rounded-lg border-outline-variant text-on-surface-variant opacity-70 text-body-base" readonly="" type="text" value="운영지원팀 (자동지정)"/>
</div>
</div>
<div>
<label class="block text-label-md text-on-surface-variant mb-2">제목</label>
<input class="w-full bg-surface-container-low border-outline-variant rounded-lg focus:border-primary focus:ring-primary text-body-base" placeholder="의견을 잘 나타낼 수 있는 제목을 입력해주세요" type="text"/>
</div>
<div>
<label class="block text-label-md text-on-surface-variant mb-2">내용</label>
<textarea class="w-full bg-surface-container-low border-outline-variant rounded-lg focus:border-primary focus:ring-primary text-body-base" placeholder="개선 사항이나 건의 내용을 구체적으로 기술해 주시면 빠른 검토가 가능합니다." rows="6"></textarea>
</div>
<div class="flex items-center justify-between pt-4">
<div class="flex space-x-2">
<button class="flex items-center px-4 py-2 border border-outline-variant text-on-surface-variant rounded-lg hover:bg-surface-container-high transition-colors font-label-md" type="button">
<span class="material-symbols-outlined mr-2" data-icon="attach_file" style="font-size: 18px;">attach_file</span>
파일 첨부
</button>
<span class="text-label-xs text-on-surface-variant flex items-center">최대 20MB (JPG, PNG, PDF 가능)</span>
</div>
<button class="bg-primary text-on-primary px-10 py-3 rounded-lg font-bold hover:bg-primary-container transition-all flex items-center shadow-md" type="submit">
<span class="material-symbols-outlined mr-2" data-icon="send">send</span>
제출하기
</button>
</div>
</form>
</div>
</div>
<!-- Info Sidebar (Bento Extra) -->
<div class="col-span-12 lg:col-span-4 space-y-6">
<div class="bg-secondary-container text-on-secondary-container rounded-xl p-6 shadow-sm border border-secondary">
<h4 class="font-title-sm mb-4 flex items-center">
<span class="material-symbols-outlined mr-2" data-icon="info">info</span>
처리 프로세스
</h4>
<ul class="space-y-4 text-label-md">
<li class="flex items-start">
<span class="bg-on-secondary-container text-secondary px-2 py-0.5 rounded mr-3 font-bold">01</span>
<span>의견 접수 후 담당 부서에서 즉시 확인 프로세스가 시작됩니다.</span>
</li>
<li class="flex items-start">
<span class="bg-on-secondary-container text-secondary px-2 py-0.5 rounded mr-3 font-bold">02</span>
<span>접수된 제안은 내부 검토를 거쳐 시스템 고도화에 반영됩니다.</span>
</li>
<li class="flex items-start">
<span class="bg-on-secondary-container text-secondary px-2 py-0.5 rounded mr-3 font-bold">03</span>
<span>처리 결과는 최대 3영업일 이내에 답변을 드립니다.</span>
</li>
</ul>
</div>
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-6">
<h4 class="font-title-sm text-on-surface mb-4">자주 묻는 질문</h4>
<div class="space-y-3">
<a class="block p-3 rounded-lg hover:bg-surface-container-low transition-colors border border-transparent hover:border-outline-variant group" href="#">
<p class="text-label-md font-bold group-hover:text-primary">시스템 권한 신청 방법은?</p>
<p class="text-label-xs text-on-surface-variant mt-1">로그인 후 설정 &gt; 권한 탭에서 가능합니다.</p>
</a>
<a class="block p-3 rounded-lg hover:bg-surface-container-low transition-colors border border-transparent hover:border-outline-variant group" href="#">
<p class="text-label-md font-bold group-hover:text-primary">실시간 데이터 갱신 주기</p>
<p class="text-label-xs text-on-surface-variant mt-1">센서 데이터는 5초 간격으로 갱신됩니다.</p>
</a>
</div>
</div>
</div>
</div>
<!-- My Submissions List Section -->
<section>
<div class="flex items-center justify-between mb-6">
<h3 class="font-headline-md text-headline-md text-on-surface">나의 문의 내역</h3>
<div class="flex space-x-2">
<button class="px-3 py-1 bg-surface-container-high rounded-full text-label-xs font-bold text-on-surface">전체</button>
<button class="px-3 py-1 text-label-xs font-bold text-on-surface-variant hover:bg-surface-container-low rounded-full">답변완료</button>
</div>
</div>
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-surface-container-low border-b border-outline-variant">
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">카테고리</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">제목</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">접수일시</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant">상태</th>
<th class="px-6 py-4 text-label-md font-bold text-on-surface-variant text-right">관리</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant">
<!-- Item 1 (Expanded Example) -->
<tr class="hover:bg-surface-container transition-colors cursor-pointer group" onclick="toggleRow('row1')">
<td class="px-6 py-4">
<span class="text-primary-container bg-primary-fixed px-2 py-1 rounded text-label-xs">개선 제안</span>
</td>
<td class="px-6 py-4 font-body-base font-medium">제1전시장 로비 키오스크 UI 반응 속도 개선 요청</td>
<td class="px-6 py-4 text-label-md text-on-surface-variant">2024.11.20 14:30</td>
<td class="px-6 py-4">
<span class="px-2 py-1 bg-tertiary-fixed text-on-tertiary-fixed rounded-full text-label-xs font-bold">답변완료</span>
</td>
<td class="px-6 py-4 text-right">
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary transition-transform" data-icon="expand_more" id="icon-row1">expand_more</span>
</td>
</tr>
<tr class="hidden bg-surface-container-low transition-all" id="row1">
<td class="px-12 py-6" colspan="5">
<div class="flex flex-col space-y-4">
<div class="bg-surface-container-lowest p-4 rounded-lg border border-outline-variant">
<p class="text-label-xs font-bold text-primary mb-1 uppercase">문의 내용</p>
<p class="text-body-base text-on-surface">제1전시장 입구에 위치한 무인 키오스크에서 데이터 로딩 시 약 3초간의 딜레이가 발생합니다. 관람객 몰리는 시간대에 병목 현상이 예상되니 최적화 부탁드립니다.</p>
</div>
<div class="ai-response-blob p-6 rounded-xl shadow-sm">
<div class="flex items-center mb-3">
<span class="material-symbols-outlined text-secondary mr-2" data-icon="smart_toy" style="font-size: 20px;">smart_toy</span>
<p class="text-label-md font-bold text-secondary">AI 요약 및 담당자 답변</p>
</div>
<div class="space-y-3">
<p class="text-body-base text-on-surface leading-relaxed">
안녕하세요. KINTEX AI 운영지원팀입니다. 제안해주신 키오스크 지연 문제는 현재 API 응답 캐싱 최적화 작업을 통해 0.5초 이내로 단축하는 업데이트가 차주 화요일 배포 예정입니다. 적극적인 의견 감사드립니다.
</p>
<div class="pt-2 border-t border-outline-variant/30 text-label-xs text-on-surface-variant italic">
이 답변은 AI 기술 지원을 통해 신속하게 생성되었으며 운영 책임자의 검토를 마쳤습니다.
</div>
</div>
</div>
</div>
</td>
</tr>
<!-- Item 2 -->
<tr class="hover:bg-surface-container transition-colors cursor-pointer group" onclick="toggleRow('row2')">
<td class="px-6 py-4">
<span class="text-on-secondary-fixed-variant bg-secondary-fixed px-2 py-1 rounded text-label-xs">불편 신고</span>
</td>
<td class="px-6 py-4 font-body-base font-medium">관리자 대시보드 모바일 뷰 깨짐 현상</td>
<td class="px-6 py-4 text-label-md text-on-surface-variant">2024.11.22 09:15</td>
<td class="px-6 py-4">
<span class="px-2 py-1 bg-primary-fixed text-primary rounded-full text-label-xs font-bold">검토중</span>
</td>
<td class="px-6 py-4 text-right">
<span class="material-symbols-outlined text-on-surface-variant" data-icon="expand_more" id="icon-row2">expand_more</span>
</td>
</tr>
<tr class="hidden bg-surface-container-low" id="row2">
<td class="px-12 py-6" colspan="5">
<div class="p-4 bg-surface-container-lowest rounded-lg border border-outline-variant">
<p class="text-body-base text-on-surface">아이폰 15 프로 환경에서 대시보드 그래프가 영역을 벗어나 표시됩니다. 긴급 수정 부탁드립니다.</p>
</div>
<div class="mt-4 flex items-center text-primary font-bold text-label-md">
<span class="material-symbols-outlined mr-2" data-icon="pending">pending</span>
담당 부서에서 해당 기기를 통해 재현 테스트 중입니다.
</div>
</td>
</tr>
<!-- Item 3 -->
<tr class="hover:bg-surface-container transition-colors cursor-pointer group">
<td class="px-6 py-4">
<span class="text-on-surface-variant bg-surface-container-high px-2 py-1 rounded text-label-xs">규정 문의</span>
</td>
<td class="px-6 py-4 font-body-base font-medium">야간 연장 운영 시 보안 인력 배치 기준</td>
<td class="px-6 py-4 text-label-md text-on-surface-variant">2024.11.23 11:00</td>
<td class="px-6 py-4">
<span class="px-2 py-1 bg-outline-variant text-on-surface-variant rounded-full text-label-xs font-bold">접수</span>
</td>
<td class="px-6 py-4 text-right">
<span class="material-symbols-outlined text-on-surface-variant" data-icon="expand_more">expand_more</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-6 flex justify-center">
<nav class="flex items-center space-x-2">
<button class="p-2 rounded-lg hover:bg-surface-container-high transition-colors"><span class="material-symbols-outlined" data-icon="chevron_left">chevron_left</span></button>
<button class="w-8 h-8 rounded-lg bg-primary text-on-primary font-bold text-label-md">1</button>
<button class="w-8 h-8 rounded-lg hover:bg-surface-container-high transition-colors text-label-md">2</button>
<button class="w-8 h-8 rounded-lg hover:bg-surface-container-high transition-colors text-label-md">3</button>
<button class="p-2 rounded-lg hover:bg-surface-container-high transition-colors"><span class="material-symbols-outlined" data-icon="chevron_right">chevron_right</span></button>
</nav>
</div>
</section>
</div>
<!-- Footer -->
<footer class="bg-surface-container-lowest dark:bg-surface-container-lowest border-t border-outline-variant flex justify-between items-center px-container-padding py-4 mt-auto">
<span class="font-label-xs text-label-xs text-on-surface-variant">© 2024 KINTEX AI 전시관리. All rights reserved.</span>
<div class="flex space-x-6">
<a class="text-on-surface-variant font-label-xs hover:underline transition-all opacity-80 hover:opacity-100" href="#">Privacy Policy</a>
<a class="text-on-surface-variant font-label-xs hover:underline transition-all opacity-80 hover:opacity-100" href="#">Terms of Service</a>
<a class="text-on-surface-variant font-label-xs hover:underline transition-all opacity-80 hover:opacity-100" href="#">Contact Support</a>
</div>
</footer>
</main>
<script>
function toggleRow(rowId) {
const row = document.getElementById(rowId);
const icon = document.getElementById('icon-' + rowId);
if (row.classList.contains('hidden')) {
row.classList.remove('hidden');
if(icon) icon.style.transform = 'rotate(180deg)';
} else {
row.classList.add('hidden');
if(icon) icon.style.transform = 'rotate(0deg)';
}
}
// Micro-interaction for sidebar links
const navLinks = document.querySelectorAll('aside nav a');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
// If it's not the active one, simulate navigation
if(!link.classList.contains('bg-secondary-container')) {
navLinks.forEach(l => {
l.classList.remove('bg-secondary-container', 'text-on-secondary-container', 'font-bold', 'border-l-4', 'border-secondary');
l.classList.add('text-on-surface-variant', 'hover:bg-surface-container-high');
});
link.classList.add('bg-secondary-container', 'text-on-secondary-container', 'font-bold', 'border-l-4', 'border-secondary');
link.classList.remove('text-on-surface-variant', 'hover:bg-surface-container-high');
}
});
});
</script>
</body></html>

View File

@ -1,413 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI 전시관리 - Unified Global Search</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-background": "#141b2c",
"on-secondary-fixed": "#1b0063",
"on-secondary": "#ffffff",
"surface-dim": "#d3d9f1",
"primary-fixed": "#d3e4ff",
"error-container": "#ffdad6",
"outline": "#727781",
"on-primary-fixed-variant": "#004881",
"surface-container-low": "#f2f3ff",
"on-secondary-fixed-variant": "#4500d8",
"surface-container-lowest": "#ffffff",
"outline-variant": "#c1c7d3",
"primary-fixed-dim": "#a2c9ff",
"surface-container": "#eaedff",
"surface-bright": "#f9f9ff",
"on-primary-container": "#91c0ff",
"secondary-container": "#6d4aff",
"tertiary-container": "#00583b",
"surface-container-high": "#e1e7ff",
"secondary-fixed": "#e5deff",
"on-tertiary-fixed": "#002113",
"on-tertiary": "#ffffff",
"background": "#faf8ff",
"surface-variant": "#dbe2fa",
"secondary": "#5427e6",
"primary-container": "#004e8b",
"on-primary": "#ffffff",
"on-tertiary-container": "#68d1a0",
"error-red": "#ba1a1a",
"secondary-fixed-dim": "#c9bfff",
"on-tertiary-fixed-variant": "#005236",
"on-surface-variant": "#424750",
"tertiary-fixed-dim": "#72daa9",
"inverse-primary": "#a2c9ff",
"on-secondary-container": "#f3edff",
"tertiary-fixed": "#8ef7c4",
"on-primary-fixed": "#001c38",
"tertiary": "#003e28",
"error": "#ba1a1a",
"on-error": "#ffffff",
"surface-tint": "#24609e",
"inverse-surface": "#293042",
"surface": "#faf8ff",
"primary": "#003765",
"on-error-container": "#93000a",
"on-surface": "#141b2c",
"surface-container-highest": "#dbe2fa",
"inverse-on-surface": "#eef0ff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-sm": "8px",
"section-padding": "96px",
"container-padding": "24px",
"base": "4px",
"stack-md": "16px",
"stack-lg": "24px",
"gutter": "16px"
},
"fontFamily": {
"label-md": ["Geist"],
"display-lg-mobile": ["Inter"],
"body-sm": ["Inter"],
"headline-md": ["Inter"],
"label-xs": ["Geist"],
"display-lg": ["Inter"],
"body-base": ["Inter"],
"title-sm": ["Inter"]
},
"fontSize": {
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
vertical-align: middle;
}
.search-highlight {
color: #004E8B;
font-weight: 700;
background-color: #d3e4ff;
padding: 0 2px;
border-radius: 2px;
}
.ai-glow {
box-shadow: 0 0 15px rgba(109, 74, 255, 0.2);
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #faf8ff;
}
::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 4px;
}
</style>
</head>
<body class="bg-surface-bright text-on-surface font-body-base antialiased">
<!-- SideNavBar Anchor -->
<aside class="h-screen w-64 fixed left-0 top-0 flex flex-col bg-surface-container dark:bg-inverse-surface border-r border-outline-variant dark:border-outline z-50">
<div class="flex flex-col h-full py-container-padding">
<div class="px-6 mb-8 flex items-center gap-3">
<div class="w-8 h-8 bg-primary rounded flex items-center justify-center">
<span class="material-symbols-outlined text-white text-lg">corporate_fare</span>
</div>
<div>
<h1 class="text-title-sm font-title-sm text-primary dark:text-primary-fixed-dim font-bold">KINTEX Admin</h1>
<p class="text-label-xs font-label-xs text-on-surface-variant">Enterprise Manager</p>
</div>
</div>
<nav class="flex-1 space-y-1 px-4 overflow-y-auto">
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-surface-variant font-body-base hover:bg-surface-container-high dark:hover:bg-surface-container-highest transition-colors rounded-lg" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span>Dashboard</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-surface-variant font-body-base hover:bg-surface-container-high dark:hover:bg-surface-container-highest transition-colors rounded-lg" href="#">
<span class="material-symbols-outlined">calendar_month</span>
<span>Exhibition Schedule</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-primary dark:text-primary-fixed-dim font-bold border-r-4 border-primary dark:border-primary-fixed-dim bg-primary-container/10 rounded-lg" href="#">
<span class="material-symbols-outlined">payments</span>
<span>Booth Sales</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-surface-variant font-body-base hover:bg-surface-container-high dark:hover:bg-surface-container-highest transition-colors rounded-lg" href="#">
<span class="material-symbols-outlined">groups</span>
<span>Exhibitor Management</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-surface-variant font-body-base hover:bg-surface-container-high dark:hover:bg-surface-container-highest transition-colors rounded-lg" href="#">
<span class="material-symbols-outlined">settings</span>
<span>Settings</span>
</a>
</nav>
<div class="px-4 mt-auto space-y-1 border-t border-outline-variant pt-6">
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-surface-variant font-body-base hover:bg-surface-container-high dark:hover:bg-surface-container-highest transition-colors rounded-lg" href="#">
<span class="material-symbols-outlined">help</span>
<span>Support</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-surface-variant font-body-base hover:bg-surface-container-high dark:hover:bg-surface-container-highest transition-colors rounded-lg" href="#">
<span class="material-symbols-outlined">logout</span>
<span>Log Out</span>
</a>
</div>
</div>
</aside>
<!-- TopNavBar Anchor -->
<header class="fixed top-0 right-0 w-[calc(100%-16rem)] h-16 z-40 bg-surface dark:bg-background border-b border-outline-variant dark:border-outline flex items-center justify-between px-gutter">
<div class="flex items-center gap-4">
<h2 class="font-headline-md text-headline-md text-primary dark:text-primary-fixed-dim">Exhibition Manager</h2>
</div>
<div class="flex items-center gap-6">
<div class="relative w-96">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant">search</span>
<input class="w-full bg-surface-container-low border border-outline-variant rounded-full py-2 pl-10 pr-4 text-body-base focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all" placeholder="통합 검색..." type="text"/>
</div>
<div class="flex items-center gap-4">
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-opacity duration-150 cursor-pointer">notifications</button>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-opacity duration-150 cursor-pointer">help</button>
<div class="w-8 h-8 rounded-full overflow-hidden border border-outline-variant">
<img class="w-full h-full object-cover" data-alt="Close-up portrait of a professional corporate administrator with a confident smile, wearing business attire, against a clean studio background with soft cinematic lighting." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDPtW6A4RdqoDHwUIG5yKCAZlpug3dKLbeG56gHpAjI2YnQu3p9XVbQBjGB_la2A9KvF56SmYDvSuh4pNEZIvp8CGJXiR2DzTT4GmUW1arLLaSYUBC6U1wGQvMNimIuO9Fcv_EsVt-Q52_fMzBTx-rRF4cSygXj3Va6RE0Jroukp3gvOgGoHCa3Ph1wcaKhudQZ7m9BsvIXEPrLhNCT7vb22mIkcS0d1U09M5jBHsS3-8Ouza9pGBbh-xV4-WHg8PGv9VUqlWzL314"/>
</div>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="ml-64 mt-16 p-stack-lg min-h-[calc(100vh-4rem)]">
<div class="max-w-7xl mx-auto space-y-stack-lg">
<!-- Centered Search Section -->
<section class="flex flex-col items-center py-12">
<div class="w-full max-w-2xl space-y-6">
<div class="relative group">
<div class="absolute inset-y-0 left-6 flex items-center pointer-events-none">
<span class="material-symbols-outlined text-primary text-2xl">search</span>
</div>
<input class="block w-full py-6 pl-16 pr-44 text-title-sm font-title-sm bg-white border-2 border-primary/20 rounded-2xl shadow-lg focus:ring-primary focus:border-primary transition-all ai-glow" placeholder="검색어를 입력하세요..." type="text" value="스마트팩토리 2026 부스 배치도"/>
<div class="absolute inset-y-3 right-3 flex items-center">
<button class="flex items-center gap-2 bg-secondary-container text-on-secondary px-6 rounded-xl font-label-md transition-transform hover:scale-[1.02] active:scale-95 shadow-md">
<img alt="AI Sparkle" class="w-5 h-5 invert" src="https://lh3.googleusercontent.com/aida/AP1WRLu1SFz3uW8s6iC8BUAS9XvAFoM_7LQZUzR-UgorcHCSIZ_uRCF-ZTLQnQbNUG13WdRaQDX_3m0ewu6jxJ2Ie-gQvwFXyYdy3ZIxhkl0hJ2VGRZF6Iul1GgmSKzO5x92HASTCa0m0XzXjctyxy5aRgxOTOxhzl-Qz9P5jTlRzXngIWh6bCNZdaVEIxd9H6a2fDdToHYg0gowaU93SV2P5YbT0-8ShD0eeNiQEFHXn4HcsQUCrsiacEeGDZY"/>
<span>AI 자연어 검색</span>
</button>
</div>
</div>
<!-- Result Tabs -->
<div class="flex justify-center border-b border-outline-variant">
<nav aria-label="Tabs" class="flex gap-8">
<button class="border-primary text-primary border-b-2 py-4 px-1 font-label-md text-label-md">전체 (128)</button>
<button class="border-transparent text-on-surface-variant hover:text-primary hover:border-outline-variant border-b-2 py-4 px-1 font-label-md text-label-md">행사 (12)</button>
<button class="border-transparent text-on-surface-variant hover:text-primary hover:border-outline-variant border-b-2 py-4 px-1 font-label-md text-label-md">부스 (45)</button>
<button class="border-transparent text-on-surface-variant hover:text-primary hover:border-outline-variant border-b-2 py-4 px-1 font-label-md text-label-md">업체 (32)</button>
<button class="border-transparent text-on-surface-variant hover:text-primary hover:border-outline-variant border-b-2 py-4 px-1 font-label-md text-label-md">문서 (24)</button>
<button class="border-transparent text-on-surface-variant hover:text-primary hover:border-outline-variant border-b-2 py-4 px-1 font-label-md text-label-md">콘텐츠 (15)</button>
</nav>
</div>
</div>
</section>
<!-- Content Grid: Facets + Results -->
<div class="grid grid-cols-12 gap-gutter">
<!-- Left Sidebar: Filters -->
<aside class="col-span-3 space-y-stack-lg">
<div class="bg-white border border-outline-variant rounded-xl p-6 space-y-8">
<div class="space-y-4">
<h3 class="font-title-sm text-label-md uppercase tracking-wider text-on-surface-variant">행사 연도</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">2026</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary font-semibold">2025</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">2024 이전</span>
</label>
</div>
</div>
<div class="space-y-4">
<h3 class="font-title-sm text-label-md uppercase tracking-wider text-on-surface-variant">상태</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer group">
<input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">진행 중</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">확정/승인</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">대기</span>
</label>
</div>
</div>
<div class="space-y-4">
<h3 class="font-title-sm text-label-md uppercase tracking-wider text-on-surface-variant">담당 부서</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">전시운영팀</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">시설관리팀</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">홍보팀</span>
</label>
</div>
</div>
<div class="space-y-4">
<h3 class="font-title-sm text-label-md uppercase tracking-wider text-on-surface-variant">파일 형식</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">PDF</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">DWG/CAD</span>
</label>
<label class="flex items-center gap-3 cursor-pointer group">
<input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary/20" type="checkbox"/>
<span class="text-body-base group-hover:text-primary">Excel</span>
</label>
</div>
</div>
</div>
</aside>
<!-- Right Side: Results List -->
<div class="col-span-9 space-y-stack-md">
<!-- AI Insight Card -->
<div class="bg-secondary-fixed/30 border-2 border-secondary-container rounded-xl p-8 relative overflow-hidden group hover:shadow-lg transition-all">
<div class="absolute top-0 right-0 w-32 h-32 -mr-8 -mt-8 opacity-10 group-hover:opacity-20 transition-opacity">
<img alt="AI Icon" class="w-full h-full" src="https://lh3.googleusercontent.com/aida/AP1WRLu1SFz3uW8s6iC8BUAS9XvAFoM_7LQZUzR-UgorcHCSIZ_uRCF-ZTLQnQbNUG13WdRaQDX_3m0ewu6jxJ2Ie-gQvwFXyYdy3ZIxhkl0hJ2VGRZF6Iul1GgmSKzO5x92HASTCa0m0XzXjctyxy5aRgxOTOxhzl-Qz9P5jTlRzXngIWh6bCNZdaVEIxd9H6a2fDdToHYg0gowaU93SV2P5YbT0-8ShD0eeNiQEFHXn4HcsQUCrsiacEeGDZY"/>
</div>
<div class="flex items-start gap-4 mb-4">
<div class="p-2 bg-secondary-container rounded-lg">
<img alt="AI Icon" class="w-6 h-6 invert" src="https://lh3.googleusercontent.com/aida/AP1WRLu1SFz3uW8s6iC8BUAS9XvAFoM_7LQZUzR-UgorcHCSIZ_uRCF-ZTLQnQbNUG13WdRaQDX_3m0ewu6jxJ2Ie-gQvwFXyYdy3ZIxhkl0hJ2VGRZF6Iul1GgmSKzO5x92HASTCa0m0XzXjctyxy5aRgxOTOxhzl-Qz9P5jTlRzXngIWh6bCNZdaVEIxd9H6a2fDdToHYg0gowaU93SV2P5YbT0-8ShD0eeNiQEFHXn4HcsQUCrsiacEeGDZY"/>
</div>
<div>
<h3 class="text-title-sm font-title-sm text-secondary">KINTEX AI Insight</h3>
<p class="text-label-xs text-on-secondary-fixed-variant">Generated Summary</p>
</div>
</div>
<p class="text-body-base leading-relaxed text-on-background mb-8">
<span class="search-highlight">스마트팩토리 2026</span> <span class="search-highlight">부스 배치도</span> 검색 결과, 작년 대비 대형 부스 신청률이 15% 증가했습니다. 제1전시장 입구 동선 주변의 부스 밀도가 높아짐에 따라 보행자 혼잡도가 예상됩니다. <span class="font-bold">A홀 4~6구역</span>의 배치를 재검토할 필요가 있으며, 관련 유사 행사 리포트 3건을 함께 추천합니다.
</p>
<div class="flex gap-4">
<button class="bg-secondary-container text-on-secondary px-6 py-2.5 rounded-full font-label-md hover:bg-secondary transition-colors shadow-sm">데이터 분석 리포트 보기</button>
<button class="border border-secondary text-secondary px-6 py-2.5 rounded-full font-label-md hover:bg-secondary-fixed transition-colors">유사 행사 배치도 비교</button>
</div>
</div>
<!-- Result Card 1 -->
<div class="bg-white border border-outline-variant border-l-4 border-l-primary rounded-xl p-6 hover:shadow-md transition-shadow group">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-2">
<div class="bg-primary-fixed text-on-primary-fixed px-3 py-1 rounded-full text-label-xs font-label-md uppercase">부스 관리</div>
<span class="text-on-surface-variant text-label-xs">• 2025.10.15 수정</span>
</div>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors">more_vert</button>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-surface-container rounded-lg flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-3xl">map</span>
</div>
<div class="flex-1">
<h4 class="text-title-sm font-title-sm group-hover:text-primary transition-colors mb-2 cursor-pointer">
<span class="search-highlight">스마트팩토리</span> <span class="search-highlight">2026</span> 제1전시장 <span class="search-highlight">부스 배치도</span> (최종안)
</h4>
<p class="text-body-base text-on-surface-variant line-clamp-2">
<span class="search-highlight">2026</span>년 3월 개최 예정인 스마트제조 혁신 전시회의 전체 레이아웃 설계안입니다. 주최측 요구사항이 반영된 A~F홀 섹터별 독립 부스 및 공동관 구획 정보가 포함되어 있습니다.
</p>
</div>
</div>
<div class="mt-4 flex items-center gap-4 text-label-xs text-on-surface-variant border-t border-outline-variant/30 pt-4">
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-sm">attachment</span> SmartFactory_2026_FloorPlan_vFinal.dwg</span>
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-sm">person</span> 전시운영팀 김철수 대리</span>
</div>
</div>
<!-- Result Card 2 (Operational) -->
<div class="bg-white border border-outline-variant border-l-4 border-l-tertiary-container rounded-xl p-6 hover:shadow-md transition-shadow group">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-2">
<div class="bg-tertiary-fixed text-on-tertiary-fixed px-3 py-1 rounded-full text-label-xs font-label-md uppercase">행사 운영</div>
<span class="text-on-surface-variant text-label-xs">• 2025.11.02 업로드</span>
</div>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors">more_vert</button>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-surface-container rounded-lg flex items-center justify-center">
<span class="material-symbols-outlined text-tertiary text-3xl">calendar_month</span>
</div>
<div class="flex-1">
<h4 class="text-title-sm font-title-sm group-hover:text-primary transition-colors mb-2 cursor-pointer">
국제 <span class="search-highlight">스마트팩토리</span> 및 산업로봇 컨퍼런스 <span class="search-highlight">2026</span> 운영 매뉴얼
</h4>
<p class="text-body-base text-on-surface-variant line-clamp-2">
행사 당일 현장 <span class="search-highlight">부스</span> 설치 및 철거 프로세스 안내서입니다. 전시장 로딩덕 접근 시간표 및 비상 대피 동선, 안전 관리 규정이 통합되어 수록되어 있습니다.
</p>
</div>
</div>
<div class="mt-4 flex items-center gap-4 text-label-xs text-on-surface-variant border-t border-outline-variant/30 pt-4">
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-sm">picture_as_pdf</span> Operations_Manual_2026.pdf</span>
<span class="flex items-center gap-1"><span class="material-symbols-outlined text-sm">person</span> 홍보팀 이영희 팀장</span>
</div>
</div>
<!-- Pagination -->
<div class="flex justify-center py-8">
<nav class="flex gap-2">
<button class="w-10 h-10 flex items-center justify-center border border-outline-variant rounded hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">chevron_left</span></button>
<button class="w-10 h-10 flex items-center justify-center bg-primary text-white rounded font-label-md">1</button>
<button class="w-10 h-10 flex items-center justify-center border border-outline-variant rounded hover:bg-surface-container transition-colors font-label-md">2</button>
<button class="w-10 h-10 flex items-center justify-center border border-outline-variant rounded hover:bg-surface-container transition-colors font-label-md">3</button>
<button class="w-10 h-10 flex items-center justify-center border border-outline-variant rounded hover:bg-surface-container transition-colors font-label-md">4</button>
<button class="w-10 h-10 flex items-center justify-center border border-outline-variant rounded hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">chevron_right</span></button>
</nav>
</div>
</div>
</div>
</div>
</main>
<script>
// Micro-interaction for search bar focusing
const searchInput = document.querySelector('input[type="text"]');
const searchContainer = searchInput.closest('.group');
searchInput.addEventListener('focus', () => {
searchContainer.classList.add('scale-[1.01]');
searchContainer.classList.add('shadow-xl');
});
searchInput.addEventListener('blur', () => {
searchContainer.classList.remove('scale-[1.01]');
searchContainer.classList.remove('shadow-xl');
});
</script>
</body></html>

View File

@ -1,437 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - SCR-45 회의록 상세</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"outline-variant": "#c1c7d3",
"on-secondary": "#ffffff",
"error-red": "#ba1a1a",
"secondary-container": "#6d4aff",
"tertiary-container": "#00583b",
"on-background": "#141b2c",
"primary-fixed": "#d3e4ff",
"surface-container-low": "#f2f3ff",
"surface-variant": "#dbe2fa",
"inverse-surface": "#293042",
"on-error": "#ffffff",
"on-error-container": "#93000a",
"surface-container-highest": "#dbe2fa",
"error-container": "#ffdad6",
"inverse-on-surface": "#eef0ff",
"on-surface-variant": "#424750",
"surface-container": "#eaedff",
"tertiary": "#003e28",
"outline": "#727781",
"on-primary-container": "#91c0ff",
"on-secondary-container": "#f3edff",
"primary": "#003765",
"on-secondary-fixed": "#1b0063",
"on-primary": "#ffffff",
"on-tertiary-fixed": "#002113",
"on-primary-fixed-variant": "#004881",
"inverse-primary": "#a2c9ff",
"secondary": "#5427e6",
"on-primary-fixed": "#001c38",
"surface-bright": "#faf8ff",
"surface": "#faf8ff",
"tertiary-fixed": "#8ef7c4",
"surface-container-high": "#e1e7ff",
"background": "#faf8ff",
"tertiary-fixed-dim": "#72daa9",
"secondary-fixed": "#e5deff",
"surface-container-lowest": "#ffffff",
"surface-dim": "#d3d9f1",
"primary-fixed-dim": "#a2c9ff",
"on-tertiary-fixed-variant": "#005236",
"on-surface": "#141b2c",
"secondary-fixed-dim": "#c9bfff",
"on-tertiary": "#ffffff",
"surface-tint": "#24609e",
"error": "#ba1a1a",
"on-secondary-fixed-variant": "#4500d8",
"on-tertiary-container": "#68d1a0",
"primary-container": "#004e8b"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-md": "16px",
"stack-lg": "24px",
"section-padding": "96px",
"stack-sm": "8px",
"container-padding": "24px",
"gutter": "16px",
"base": "4px"
},
"fontFamily": {
"title-sm": ["Inter"],
"label-md": ["Geist"],
"display-lg": ["Inter"],
"headline-md": ["Inter"],
"body-sm": ["Inter"],
"body-base": ["Inter"]
},
"fontSize": {
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c7d3;
border-radius: 10px;
}
</style>
</head>
<body class="bg-surface-bright text-on-surface font-body-base overflow-hidden">
<!-- SideNavBar -->
<aside class="w-[240px] h-screen fixed left-0 top-0 border-r border-outline-variant bg-surface-container-lowest flex flex-col py-6 z-50">
<div class="px-6 mb-8">
<img alt="KINTEX AI Logo" class="h-8 object-contain" src="https://lh3.googleusercontent.com/aida/AP1WRLuPIxmrE2KEEIZK1TQvP8jxtO3MkEPMwc1tVCXQeurK2ywZzqrmszTWhS-MOk7l0NlBT-fBPr7AhpqDh_Us84e3UMXjCij089BjFJFtd8hUFB4nCRvA34TPEVBBTUOI57NDo3tHcwfds6dQIDxogGQ3h7AjQhNuygBIiOTVTVUBEtlzz7vP_MViWoBmnjyisUQcYXxLs8rK6MRbhhGH4FpRm8pylR9CssnJFPS2I6dvpRX4NSTCyvpubg"/>
<p class="font-label-md text-label-md text-on-surface-variant mt-2">Meeting Management</p>
</div>
<nav class="flex-1 px-2 space-y-1">
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors" href="#">
<span class="material-symbols-outlined mr-3" data-icon="dashboard">dashboard</span>
<span class="font-label-md text-label-md">대시보드</span>
</a>
<a class="flex items-center px-4 py-3 bg-secondary-container text-on-secondary-container rounded-full transition-all" href="#">
<span class="material-symbols-outlined mr-3" data-icon="description">description</span>
<span class="font-label-md text-label-md">회의록</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors" href="#">
<span class="material-symbols-outlined mr-3" data-icon="groups">groups</span>
<span class="font-label-md text-label-md">참석자 관리</span>
</a>
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors" href="#">
<span class="material-symbols-outlined mr-3" data-icon="settings">settings</span>
<span class="font-label-md text-label-md">설정</span>
</a>
</nav>
<div class="px-4 mt-auto">
<button class="w-full bg-primary text-on-primary py-3 px-4 rounded-xl font-label-md text-label-md hover:opacity-90 transition-all flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]" data-icon="add_circle">add_circle</span>
새 회의 시작
</button>
</div>
</aside>
<!-- TopAppBar -->
<header class="fixed top-0 right-0 left-[240px] h-16 bg-surface-bright border-b border-outline-variant flex justify-between items-center px-gutter z-40">
<div class="flex items-center gap-4">
<div class="flex items-center text-on-surface-variant text-body-sm font-body-sm">
<span>사전업무협의</span>
<span class="material-symbols-outlined mx-1 text-sm" data-icon="chevron_right">chevron_right</span>
<span class="text-primary font-bold">회의록 상세 (SCR-45)</span>
</div>
</div>
<div class="flex items-center gap-4">
<button class="bg-primary-container text-on-primary-container px-4 py-2 rounded-lg font-label-md text-label-md flex items-center gap-2 hover:opacity-90 transition-all">
<span class="material-symbols-outlined text-[18px]" data-icon="picture_as_pdf">picture_as_pdf</span>
회의록 PDF 내보내기
</button>
<div class="flex items-center gap-3 ml-4 border-l pl-4 border-outline-variant">
<button class="text-on-surface-variant hover:text-primary transition-colors">
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
</button>
<div class="w-8 h-8 rounded-full bg-surface-container-highest flex items-center justify-center overflow-hidden border border-outline-variant">
<span class="material-symbols-outlined text-on-surface-variant" data-icon="account_circle">account_circle</span>
</div>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="ml-[240px] mt-16 h-[calc(100vh-64px)] flex overflow-hidden">
<!-- Left Panel: Meeting List -->
<section class="w-[320px] bg-surface border-r border-outline-variant flex flex-col overflow-hidden">
<div class="p-4 border-b border-outline-variant">
<div class="relative">
<input class="w-full bg-surface-container-low border border-outline-variant rounded-full py-2 pl-10 pr-4 text-body-sm focus:ring-1 focus:ring-primary focus:outline-none" placeholder="회의명, 일시 검색" type="text"/>
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]" data-icon="search">search</span>
</div>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar">
<!-- SCR-45 Active Item -->
<div class="p-4 bg-secondary-fixed text-on-secondary-fixed-variant border-l-4 border-secondary-container cursor-pointer transition-colors relative">
<div class="flex justify-between items-start mb-1">
<span class="font-label-md text-label-md opacity-70">SCR-45</span>
<span class="bg-error-red text-white text-[10px] font-bold px-1.5 py-0.5 rounded">D-30</span>
</div>
<h3 class="font-title-sm text-[15px] leading-tight mb-2">KINTEX AI 전시관리 기획 회의</h3>
<div class="flex items-center gap-2 text-label-md opacity-80">
<span class="material-symbols-outlined text-[14px]" data-icon="calendar_month">calendar_month</span>
2024.05.15 14:00
</div>
</div>
<!-- SCR-44 Item -->
<div class="p-4 hover:bg-surface-container cursor-pointer transition-colors border-b border-outline-variant/30">
<div class="flex justify-between items-start mb-1">
<span class="font-label-md text-label-md text-on-surface-variant">SCR-44</span>
</div>
<h3 class="font-title-sm text-[15px] leading-tight mb-2 text-on-surface">네트워크 인프라 구축 2차 미팅</h3>
<div class="flex items-center gap-2 text-label-md text-on-surface-variant">
<span class="material-symbols-outlined text-[14px]" data-icon="calendar_month">calendar_month</span>
2024.05.12 10:30
</div>
</div>
<!-- SCR-43 Item -->
<div class="p-4 hover:bg-surface-container cursor-pointer transition-colors border-b border-outline-variant/30">
<div class="flex justify-between items-start mb-1">
<span class="font-label-md text-label-md text-on-surface-variant">SCR-43</span>
</div>
<h3 class="font-title-sm text-[15px] leading-tight mb-2 text-on-surface">보안 가이드라인 수립 회의</h3>
<div class="flex items-center gap-2 text-label-md text-on-surface-variant">
<span class="material-symbols-outlined text-[14px]" data-icon="calendar_month">calendar_month</span>
2024.05.10 16:00
</div>
</div>
<!-- SCR-42 Item -->
<div class="p-4 hover:bg-surface-container cursor-pointer transition-colors border-b border-outline-variant/30">
<div class="flex justify-between items-start mb-1">
<span class="font-label-md text-label-md text-on-surface-variant">SCR-42</span>
</div>
<h3 class="font-title-sm text-[15px] leading-tight mb-2 text-on-surface">UI/UX 최종 컨셉 리뷰</h3>
<div class="flex items-center gap-2 text-label-md text-on-surface-variant">
<span class="material-symbols-outlined text-[14px]" data-icon="calendar_month">calendar_month</span>
2024.05.08 13:00
</div>
</div>
</div>
</section>
<!-- Right Panel: Detail View -->
<section class="flex-1 flex flex-col bg-surface-bright overflow-hidden relative">
<!-- Audio Player -->
<div class="bg-surface-container-lowest border-b border-outline-variant p-4 flex items-center gap-6">
<button class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center hover:scale-105 transition-transform">
<span class="material-symbols-outlined" data-icon="play_arrow" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
</button>
<div class="flex-1">
<div class="flex justify-between text-label-md text-on-surface-variant mb-1 px-1">
<span>04:15</span>
<span>42:30</span>
</div>
<div class="w-full h-1.5 bg-surface-container rounded-full overflow-hidden relative">
<div class="absolute top-0 left-0 h-full bg-secondary w-[10%]"></div>
<div class="absolute top-1/2 left-[10%] -translate-y-1/2 w-3 h-3 bg-white border-2 border-secondary rounded-full shadow-md cursor-pointer"></div>
</div>
</div>
<div class="flex items-center gap-4 border-l pl-4 border-outline-variant">
<div class="flex items-center gap-1 text-on-surface-variant hover:text-primary cursor-pointer">
<span class="material-symbols-outlined text-[20px]" data-icon="volume_up">volume_up</span>
</div>
<select class="bg-transparent border-none text-label-md font-bold focus:ring-0 cursor-pointer">
<option>1.0x</option>
<option>1.2x</option>
<option>1.5x</option>
<option>2.0x</option>
</select>
</div>
</div>
<!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-6">
<!-- AI Summary Section -->
<div class="bg-on-secondary-container/5 border-2 border-secondary-container/20 rounded-2xl p-6 relative overflow-hidden">
<div class="absolute top-0 right-0 opacity-10 pointer-events-none p-4">
<img alt="Analytics Icon" class="w-24 h-24" src="https://lh3.googleusercontent.com/aida/AP1WRLujD1FMRHChfM_aVKt2cd12chhcnm31L8Ks2EAzo-_FQ1_RV3zmYa4WUlSJ67wYexda4SpJU88Y5L0HdiYx6fLuKFdsDKjKWm1SmTq4PIP2tx1Nys-Txdlq4Wc1keg1GKsgrxNY68bHBEhurr-0NorgLbpp032gBkj_vE345bYKJwSr5MEnipvo_1P7iE9cXEBybLmdLLYK6vM1nB7dbmVt9_NHG3NQgK7wcPfIaNBrnXrEqtZeckWX3v8"/>
</div>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-xl bg-secondary-container flex items-center justify-center shadow-md">
<span class="material-symbols-outlined text-on-secondary-container" data-icon="auto_awesome" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
</div>
<h2 class="font-headline-md text-[18px] text-secondary">AI 요약 핵심 내용</h2>
</div>
<ul class="space-y-3">
<li class="flex items-start gap-3">
<div class="mt-1.5 w-1.5 h-1.5 rounded-full bg-secondary flex-shrink-0"></div>
<p class="text-body-base leading-relaxed"><strong class="text-secondary">트래픽 모니터링 시스템:</strong> 실시간 대규모 인원 유입에 따른 병목 구간 자동 감지 및 분산 가이드라인 수립.</p>
</li>
<li class="flex items-start gap-3">
<div class="mt-1.5 w-1.5 h-1.5 rounded-full bg-secondary flex-shrink-0"></div>
<p class="text-body-base leading-relaxed"><strong class="text-secondary">클라우드 지연 시간 최적화:</strong> 에지 서버 배치를 통한 데이터 전송 지연 20ms 이내 단축 목표 합의.</p>
</li>
<li class="flex items-start gap-3">
<div class="mt-1.5 w-1.5 h-1.5 rounded-full bg-secondary flex-shrink-0"></div>
<p class="text-body-base leading-relaxed"><strong class="text-secondary">파일럿 테스트 일정:</strong> 6월 1주차부터 1전시장 B홀에서 실환경 성능 검증 단계 진입 결정.</p>
</li>
</ul>
</div>
<!-- STT Transcription Section -->
<div>
<div class="flex items-center justify-between mb-4 px-2">
<h2 class="font-headline-md text-[18px] flex items-center gap-2">
<span class="material-symbols-outlined text-primary" data-icon="stt">rtt</span>
STT 회의 전사
</h2>
<span class="text-label-md text-on-surface-variant bg-surface-container px-3 py-1 rounded-full">정확도 96.8%</span>
</div>
<div class="space-y-6 px-2">
<!-- Speaker A -->
<div class="flex gap-4">
<div class="w-10 h-10 rounded-full bg-primary-fixed flex-shrink-0 flex items-center justify-center font-bold text-on-primary-fixed">A</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-bold text-body-sm">참석자 A (김민수 대리)</span>
<span class="text-label-xs text-on-surface-variant">04:15</span>
</div>
<div class="bg-surface-container-lowest border border-outline-variant rounded-2xl rounded-tl-none p-4 shadow-sm">
<p class="text-body-base leading-relaxed">
이지현 팀장님, 이번 AI 전시관리 시스템의 핵심 모듈인 트래픽 예측 알고리즘에 대해 설명 부탁드립니다.
특히 이번 KINTEX 대형 행사에서 예상되는 동시 접속자 처리 용량이 어느 정도 수준인지 궁금합니다.
</p>
</div>
</div>
</div>
<!-- AI Assistant Inline -->
<div class="flex justify-center my-4">
<div class="bg-secondary-container text-on-secondary-container px-6 py-3 rounded-full flex items-center gap-3 shadow-lg transform hover:scale-[1.02] transition-transform">
<span class="material-symbols-outlined animate-pulse" data-icon="smart_toy" style="font-variation-settings: 'FILL' 1;">smart_toy</span>
<p class="text-body-sm">
<strong class="mr-2">분석 완료:</strong> 현재 논의 중인 트래픽 예측 모듈은 <span class="underline decoration-2">98.2%의 정확도</span>를 보이며, 초당 15,000건의 이벤트를 지연 없이 처리 가능합니다.
</p>
</div>
</div>
<!-- Speaker B -->
<div class="flex gap-4 flex-row-reverse text-right">
<div class="w-10 h-10 rounded-full bg-secondary-fixed flex-shrink-0 flex items-center justify-center font-bold text-on-secondary-fixed">B</div>
<div class="flex-1">
<div class="flex items-center justify-end gap-2 mb-1">
<span class="text-label-xs text-on-surface-variant">04:42</span>
<span class="font-bold text-body-sm">참석자 B (이지현 팀장)</span>
</div>
<div class="bg-secondary-fixed/30 border border-secondary-container/20 rounded-2xl rounded-tr-none p-4 shadow-sm inline-block max-w-[80%]">
<p class="text-body-base leading-relaxed">
네, 민수 대리님. 현재 설계된 알고리즘은 강화학습 기반으로 유동인구의 흐름을 5분 단위로 예측합니다.
KINTEX 1, 2전시장 통합 시 약 10만 명의 실시간 위치 데이터를 안정적으로 소화할 수 있도록 아키텍처를 잡았습니다.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Action Items Table -->
<div class="mt-8 bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
<div class="bg-surface-container-low px-6 py-4 border-b border-outline-variant flex items-center gap-2">
<span class="material-symbols-outlined text-primary" data-icon="task_alt">task_alt</span>
<h2 class="font-title-sm text-[16px]">후속 조치 사항 (Action Items)</h2>
</div>
<table class="w-full text-left">
<thead class="bg-surface-bright border-b border-outline-variant">
<tr>
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">할 일</th>
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">담당자</th>
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">기한</th>
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant text-center">연결 서비스</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant/30">
<tr class="hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-5 h-5 border-2 border-outline rounded flex items-center justify-center cursor-pointer hover:bg-primary-container"></div>
<span class="text-body-base font-medium">AI 알고리즘 파일럿 테스트 계획서 작성</span>
</div>
</td>
<td class="px-6 py-4 text-body-sm">김민수 대리</td>
<td class="px-6 py-4 text-body-sm">2024.05.20</td>
<td class="px-6 py-4 text-center">
<button class="bg-surface-container-high text-on-surface hover:bg-primary hover:text-on-primary px-3 py-1.5 rounded-lg text-label-md transition-all inline-flex items-center gap-1.5">
<span class="material-symbols-outlined text-[16px]" data-icon="description">description</span>
서류
</button>
</td>
</tr>
<tr class="hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-5 h-5 border-2 border-outline rounded flex items-center justify-center cursor-pointer hover:bg-primary-container"></div>
<span class="text-body-base font-medium">엣지 서버 하드웨어 발주 및 설치</span>
</div>
</td>
<td class="px-6 py-4 text-body-sm">이지현 팀장</td>
<td class="px-6 py-4 text-body-sm">2024.05.25</td>
<td class="px-6 py-4 text-center">
<button class="bg-surface-container-high text-on-surface hover:bg-primary hover:text-on-primary px-3 py-1.5 rounded-lg text-label-md transition-all inline-flex items-center gap-1.5">
<span class="material-symbols-outlined text-[16px]" data-icon="event">event</span>
일정
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="h-12"></div> <!-- Padding for bottom bar -->
</div>
<!-- Bottom Input Bar -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-surface-bright via-surface-bright to-transparent pointer-events-none">
<div class="max-w-4xl mx-auto w-full flex items-center gap-3 bg-surface-container-lowest border border-outline-variant p-2 pl-4 rounded-full shadow-lg pointer-events-auto ring-4 ring-white">
<button class="text-on-surface-variant hover:text-primary transition-colors">
<span class="material-symbols-outlined" data-icon="attachment">attachment</span>
</button>
<input class="flex-1 bg-transparent border-none focus:ring-0 text-body-base" placeholder="회의 내용에 대해 질문하거나 의견을 추가하세요..." type="text"/>
<button class="bg-primary text-on-primary w-10 h-10 rounded-full flex items-center justify-center hover:scale-105 transition-transform shadow-md">
<span class="material-symbols-outlined" data-icon="send" style="font-variation-settings: 'FILL' 1;">send</span>
</button>
</div>
</div>
</section>
</main>
<script>
// Micro-interaction for Playback progress
document.addEventListener('DOMContentLoaded', () => {
const progressBar = document.querySelector('.w-full.h-1.5.bg-surface-container');
const progressFill = document.querySelector('.bg-secondary.w-\\[10\\%\\]');
const progressHandle = document.querySelector('.left-\\[10\\%\\]');
if (progressBar) {
progressBar.addEventListener('click', (e) => {
const rect = progressBar.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width * 100;
progressFill.style.width = `${percent}%`;
progressHandle.style.left = `${percent}%`;
});
}
// Simple search filter interaction
const searchInput = document.querySelector('input[placeholder*="검색"]');
const meetingItems = document.querySelectorAll('section.w-\\[320px\\] .cursor-pointer');
searchInput.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
meetingItems.forEach(item => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(term) ? 'block' : 'none';
});
});
});
</script>
</body></html>

View File

@ -1,452 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="ko"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>KINTEX AI - 업무보고 통계 (SCR-46)</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Geist:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-tint": "#24609e",
"on-secondary-fixed": "#1b0063",
"secondary-fixed-dim": "#c9bfff",
"error-container": "#ffdad6",
"tertiary-container": "#00583b",
"surface-bright": "#f9f9ff",
"surface-container-low": "#f2f3ff",
"error-red": "#ba1a1a",
"outline-variant": "#c1c7d3",
"on-surface-variant": "#424750",
"on-primary": "#ffffff",
"on-error-container": "#93000a",
"on-tertiary": "#ffffff",
"primary-fixed-dim": "#a2c9ff",
"on-surface": "#141b2c",
"surface": "#faf8ff",
"on-primary-container": "#91c0ff",
"primary-fixed": "#d3e4ff",
"surface-variant": "#dbe2fa",
"on-tertiary-fixed": "#002113",
"on-secondary-container": "#f3edff",
"surface-container-highest": "#dbe2fa",
"surface-container": "#eaedff",
"tertiary": "#003e28",
"outline": "#727781",
"on-error": "#ffffff",
"on-primary-fixed": "#001c38",
"on-primary-fixed-variant": "#004881",
"surface-container-lowest": "#ffffff",
"secondary": "#5427e6",
"on-secondary-fixed-variant": "#4500d8",
"on-background": "#141b2c",
"surface-dim": "#d3d9f1",
"secondary-container": "#6d4aff",
"tertiary-fixed": "#8ef7c4",
"inverse-primary": "#a2c9ff",
"surface-container-high": "#e1e7ff",
"inverse-on-surface": "#eef0ff",
"inverse-surface": "#293042",
"on-secondary": "#ffffff",
"error": "#ba1a1a",
"on-tertiary-fixed-variant": "#005236",
"on-tertiary-container": "#68d1a0",
"primary-container": "#004e8b",
"tertiary-fixed-dim": "#72daa9",
"primary": "#003765",
"background": "#faf8ff",
"secondary-fixed": "#e5deff"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"section-padding": "96px",
"container-padding": "24px",
"stack-md": "16px",
"gutter": "16px",
"stack-sm": "8px",
"stack-lg": "24px",
"base": "4px"
},
"fontFamily": {
"title-sm": ["Inter"],
"headline-md": ["Inter"],
"label-xs": ["Geist"],
"label-md": ["Geist"],
"display-lg-mobile": ["Inter"],
"body-base": ["Inter"],
"display-lg": ["Inter"],
"body-sm": ["Inter"]
},
"fontSize": {
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', 'Geist', sans-serif; background-color: #f9f9ff; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.tabular-nums { font-variant-numeric: tabular-nums; }
.chart-grid { background-image: radial-gradient(#c1c7d3 0.5px, transparent 0.5px); background-size: 20px 20px; }
</style>
</head>
<body class="bg-surface-bright text-on-background">
<!-- SideNavBar (Fixed Shell) -->
<aside class="fixed left-0 top-0 bottom-0 z-40 w-[240px] bg-surface-container-lowest border-r border-outline-variant flex flex-col">
<div class="p-6">
<h1 class="font-headline-md text-headline-md font-bold text-primary">KINTEX AI</h1>
<p class="font-body-sm text-on-surface-variant">전시관리 시스템</p>
</div>
<nav class="flex-1 px-4 space-y-1">
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-body-base">대시보드</span>
</a>
<!-- Active State: 업무보고 -->
<a class="flex items-center gap-3 px-4 py-3 bg-primary-fixed text-on-primary-fixed font-bold rounded-lg transition-all duration-200 active:scale-95" href="#">
<span class="material-symbols-outlined">description</span>
<span class="font-body-base">업무보고</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
<span class="material-symbols-outlined">analytics</span>
<span class="font-body-base">통계분석</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
<span class="material-symbols-outlined">business_center</span>
<span class="font-body-base">시설관리</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-body-base">설정</span>
</a>
</nav>
<div class="p-4 border-t border-outline-variant flex items-center gap-3">
<img class="w-10 h-10 rounded-full bg-surface-container" data-alt="A professional headshot of a Korean facility manager wearing a clean corporate uniform, high-key studio lighting, modern minimalist background, 8k resolution, crisp details." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBcDb6o4O4oJHaZPzqG0ivB2vNsIdfTum6O5X1YvE-nQRIvw5vjf641q41SXMBC8FBjuejqf5ko5YdgLVI7-EY8UvbUxtswy7lwoDS7GC7T7H0rBuHvDdtdAW74baRuwsPAY2RUZK7J3tPTuTYUdl5bUEK7Up8A_gtt3lMct4V3KpTnkY7LpTtYIorrS-WOvKWmB9cCM8Bzgt_MiHQU7Ned91oud3R-ZQ_WHlh2VLREnYXmZTGvfIPDTPUqJqa8BwawO9og41IZvsA"/>
<div class="overflow-hidden">
<p class="font-label-md text-on-surface truncate">관리자 김진희</p>
<p class="font-label-xs text-on-surface-variant truncate">운영기획팀</p>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-[240px] flex flex-col min-h-screen">
<!-- TopAppBar -->
<header class="sticky top-0 z-30 flex justify-between items-center w-full px-8 h-16 bg-surface-bright border-b border-outline-variant">
<div class="flex items-center gap-8">
<span class="font-title-sm text-title-sm font-bold text-primary">KINTEX AI 업무보고 통계</span>
<nav class="flex gap-6 mt-1">
<button class="font-title-sm text-title-sm text-on-surface-variant pb-2 hover:text-primary transition-colors cursor-pointer">일간</button>
<!-- Active State: 주간 -->
<button class="font-title-sm text-title-sm text-primary font-bold border-b-2 border-primary pb-2 cursor-pointer">주간</button>
<button class="font-title-sm text-title-sm text-on-surface-variant pb-2 hover:text-primary transition-colors cursor-pointer">월간</button>
<button class="font-title-sm text-title-sm text-on-surface-variant pb-2 hover:text-primary transition-colors cursor-pointer">분기</button>
<button class="font-title-sm text-title-sm text-on-surface-variant pb-2 hover:text-primary transition-colors cursor-pointer">연간</button>
</nav>
</div>
<div class="flex items-center gap-4">
<button class="flex items-center gap-2 px-4 py-2 bg-primary text-on-primary rounded-lg font-label-md hover:bg-primary-container transition-all">
<span class="material-symbols-outlined text-[18px]">download</span>
보고서 PDF
</button>
<div class="flex items-center gap-2 text-on-surface-variant">
<span class="material-symbols-outlined cursor-pointer hover:text-primary p-1">notifications</span>
<span class="material-symbols-outlined cursor-pointer hover:text-primary p-1">account_circle</span>
</div>
</div>
</header>
<!-- Canvas -->
<div class="p-8 max-w-[1280px] w-full mx-auto space-y-gutter">
<!-- KPI Cards -->
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-gutter">
<!-- KPI 1 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5 border-l-4 border-l-primary flex flex-col justify-between h-32 transition-all hover:shadow-md">
<span class="font-label-md text-on-surface-variant">총 업무 건수</span>
<div class="flex items-end justify-between">
<span class="font-display-lg text-display-lg tabular-nums">1,284</span>
<span class="font-label-md text-primary bg-primary-fixed px-2 py-0.5 rounded flex items-center">
<span class="material-symbols-outlined text-sm">trending_up</span> 12%
</span>
</div>
</div>
<!-- KPI 2 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5 border-l-4 border-l-secondary flex flex-col justify-between h-32 transition-all hover:shadow-md">
<span class="font-label-md text-on-surface-variant">완료율</span>
<div class="flex items-end justify-between">
<span class="font-display-lg text-display-lg tabular-nums">94.2%</span>
<span class="font-label-md text-on-secondary-fixed-variant bg-secondary-fixed px-2 py-0.5 rounded flex items-center">
<span class="material-symbols-outlined text-sm">check</span>
</span>
</div>
</div>
<!-- KPI 3 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5 border-l-4 border-l-tertiary flex flex-col justify-between h-32 transition-all hover:shadow-md">
<span class="font-label-md text-on-surface-variant">평균 처리 시간</span>
<div class="flex items-end justify-between">
<span class="font-display-lg text-display-lg tabular-nums">1.4h</span>
<span class="font-label-md text-on-tertiary-fixed-variant bg-tertiary-fixed px-2 py-0.5 rounded flex items-center">
<span class="material-symbols-outlined text-sm">speed</span>
</span>
</div>
</div>
<!-- KPI 4 -->
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5 border-l-4 border-l-error-red flex flex-col justify-between h-32 transition-all hover:shadow-md">
<span class="font-label-md text-on-surface-variant">AI 자동화 비중</span>
<div class="flex items-end justify-between">
<span class="font-display-lg text-display-lg tabular-nums">68%</span>
<span class="font-label-md text-on-error-container bg-error-container px-2 py-0.5 rounded flex items-center">
<span class="material-symbols-outlined text-sm">smart_toy</span>
</span>
</div>
</div>
</section>
<!-- Chart Area -->
<section class="bg-surface-container-lowest border border-outline-variant rounded-xl p-6 transition-all hover:shadow-md">
<div class="flex justify-between items-center mb-8">
<div>
<h3 class="font-title-sm text-title-sm text-on-surface">작업량 추이 (Work Volume Trend)</h3>
<p class="font-body-sm text-on-surface-variant">최근 7일간의 주요 업무 카테고리별 발생 건수</p>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-1.5 font-label-md">
<div class="w-3 h-3 rounded-full bg-primary"></div><span>시설</span>
</div>
<div class="flex items-center gap-1.5 font-label-md">
<div class="w-3 h-3 rounded-full bg-secondary"></div><span>보안</span>
</div>
<div class="flex items-center gap-1.5 font-label-md">
<div class="w-3 h-3 rounded-full bg-tertiary-fixed-dim"></div><span>안내</span>
</div>
</div>
</div>
<!-- Visualizing a bar chart with CSS for Recharts aesthetic -->
<div class="h-64 chart-grid relative flex items-end justify-between px-8 border-b border-l border-outline-variant">
<!-- Day columns -->
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[60%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-1/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-2/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">월요일</span>
</div>
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[75%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-3/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-1/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-1/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">화요일</span>
</div>
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[50%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-1/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">수요일</span>
</div>
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[85%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-1/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">목요일</span>
</div>
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[95%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-3/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-1/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-1/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">금요일</span>
</div>
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[40%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-1/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-2/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">토요일</span>
</div>
<div class="flex-1 flex flex-col items-center justify-end gap-1 h-full px-4 group">
<div class="w-full flex flex-col justify-end gap-0.5 h-[30%]">
<div class="bg-primary-fixed-dim hover:bg-primary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-secondary-fixed-dim hover:bg-secondary h-2/5 rounded-t-sm transition-all"></div>
<div class="bg-tertiary-fixed-dim hover:bg-tertiary h-1/5 rounded-t-sm transition-all"></div>
</div>
<span class="absolute -bottom-8 font-label-md text-on-surface-variant">일요일</span>
</div>
</div>
<div class="h-10"></div>
</section>
<!-- Breakdown Table -->
<section class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden transition-all hover:shadow-md">
<div class="p-6 border-b border-outline-variant flex justify-between items-center">
<h3 class="font-title-sm text-title-sm text-on-surface">상세 업무 보고 내역</h3>
<div class="flex gap-2">
<div class="relative">
<input class="pl-9 pr-4 py-2 border border-outline-variant rounded-full font-body-sm bg-surface-container-low focus:ring-2 focus:ring-primary outline-none" placeholder="작성자 검색" type="text"/>
<span class="material-symbols-outlined absolute left-3 top-2.5 text-[18px] text-on-surface-variant">search</span>
</div>
<button class="px-3 py-2 border border-outline-variant rounded-full font-label-md text-on-surface-variant flex items-center gap-1 hover:bg-surface-container-high">
<span class="material-symbols-outlined text-[18px]">filter_list</span> 필터
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-surface-container text-on-surface-variant font-label-md uppercase tracking-wider">
<th class="px-6 py-4">ID</th>
<th class="px-6 py-4">작성자</th>
<th class="px-6 py-4">업무 유형</th>
<th class="px-6 py-4">주요 내용</th>
<th class="px-6 py-4 text-right">건수</th>
<th class="px-6 py-4">상태</th>
<th class="px-6 py-4">최종 업데이트</th>
</tr>
</thead>
<tbody class="font-body-sm text-on-surface divide-y divide-outline-variant">
<!-- Row 1 -->
<tr class="hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4 tabular-nums text-on-surface-variant">#WR-1029</td>
<td class="px-6 py-4 flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-primary-fixed flex items-center justify-center text-[10px] font-bold text-on-primary-fixed">MK</div>
민경호
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded bg-primary-fixed text-on-primary-fixed-variant text-[11px] font-bold">시설</span>
</td>
<td class="px-6 py-4 truncate max-w-xs">1전시장 3홀 냉난방 장치 정기 점검 완료</td>
<td class="px-6 py-4 text-right tabular-nums font-medium">12</td>
<td class="px-6 py-4">
<span class="flex items-center gap-1 text-on-tertiary-fixed-variant">
<span class="w-2 h-2 rounded-full bg-tertiary-fixed-dim"></span> 완료
</span>
</td>
<td class="px-6 py-4 text-on-surface-variant tabular-nums">2024.05.21 14:20</td>
</tr>
<!-- Row 2 -->
<tr class="bg-surface-container-lowest hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4 tabular-nums text-on-surface-variant">#WR-1030</td>
<td class="px-6 py-4 flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-secondary-fixed flex items-center justify-center text-[10px] font-bold text-on-secondary-fixed">ST</div>
성태호
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded bg-secondary-fixed text-on-secondary-fixed-variant text-[11px] font-bold">보안</span>
</td>
<td class="px-6 py-4 truncate max-w-xs">야간 순찰 및 출입 통제 구역 시스템 로그 분석</td>
<td class="px-6 py-4 text-right tabular-nums font-medium">45</td>
<td class="px-6 py-4">
<span class="flex items-center gap-1 text-primary">
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span> 진행중
</span>
</td>
<td class="px-6 py-4 text-on-surface-variant tabular-nums">2024.05.21 15:05</td>
</tr>
<!-- Row 3 -->
<tr class="hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4 tabular-nums text-on-surface-variant">#WR-1031</td>
<td class="px-6 py-4 flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-tertiary-fixed flex items-center justify-center text-[10px] font-bold text-on-tertiary-fixed">JH</div>
정현우
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded bg-tertiary-fixed text-on-tertiary-fixed-variant text-[11px] font-bold">안내</span>
</td>
<td class="px-6 py-4 truncate max-w-xs">VIP 컨퍼런스 의전 및 챗봇 연동 고객 문의 대응</td>
<td class="px-6 py-4 text-right tabular-nums font-medium">108</td>
<td class="px-6 py-4">
<span class="flex items-center gap-1 text-on-tertiary-fixed-variant">
<span class="w-2 h-2 rounded-full bg-tertiary-fixed-dim"></span> 완료
</span>
</td>
<td class="px-6 py-4 text-on-surface-variant tabular-nums">2024.05.21 16:30</td>
</tr>
<!-- Row 4 -->
<tr class="bg-surface-container-lowest hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4 tabular-nums text-on-surface-variant">#WR-1032</td>
<td class="px-6 py-4 flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-surface-variant flex items-center justify-center text-[10px] font-bold text-on-surface-variant">MS</div>
미화팀
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded bg-surface-variant text-on-surface-variant text-[11px] font-bold">미화</span>
</td>
<td class="px-6 py-4 truncate max-w-xs">전시 종료 후 메인 로비 방역 및 카펫 크리닝</td>
<td class="px-6 py-4 text-right tabular-nums font-medium">8</td>
<td class="px-6 py-4">
<span class="flex items-center gap-1 text-error-red">
<span class="w-2 h-2 rounded-full bg-error-red"></span> 지연
</span>
</td>
<td class="px-6 py-4 text-on-surface-variant tabular-nums">2024.05.21 16:45</td>
</tr>
<!-- Row 5 -->
<tr class="hover:bg-surface-container-low transition-colors">
<td class="px-6 py-4 tabular-nums text-on-surface-variant">#WR-1033</td>
<td class="px-6 py-4 flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-primary-fixed flex items-center justify-center text-[10px] font-bold text-on-primary-fixed">CS</div>
최승우
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded bg-primary-fixed text-on-primary-fixed-variant text-[11px] font-bold">시설</span>
</td>
<td class="px-6 py-4 truncate max-w-xs">옥상 태양광 패널 효율 측정 및 클라우드 데이터 전송</td>
<td class="px-6 py-4 text-right tabular-nums font-medium">2</td>
<td class="px-6 py-4">
<span class="flex items-center gap-1 text-primary">
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span> 진행중
</span>
</td>
<td class="px-6 py-4 text-on-surface-variant tabular-nums">2024.05.21 17:10</td>
</tr>
</tbody>
</table>
</div>
<div class="p-4 bg-surface-container-low flex justify-between items-center border-t border-outline-variant">
<span class="font-label-xs text-on-surface-variant">Showing 1 to 5 of 124 entries</span>
<div class="flex gap-2">
<button class="px-3 py-1 border border-outline-variant rounded font-label-md hover:bg-surface-container-high disabled:opacity-50" disabled="">이전</button>
<button class="px-3 py-1 bg-primary text-on-primary rounded font-label-md">1</button>
<button class="px-3 py-1 border border-outline-variant rounded font-label-md hover:bg-surface-container-high">2</button>
<button class="px-3 py-1 border border-outline-variant rounded font-label-md hover:bg-surface-container-high">3</button>
<button class="px-3 py-1 border border-outline-variant rounded font-label-md hover:bg-surface-container-high">다음</button>
</div>
</div>
</section>
</div>
</main>
<script>
// Simple micro-interaction for chart bars
document.querySelectorAll('.group div[class*="bg-"]').forEach(bar => {
bar.addEventListener('mouseenter', () => {
bar.style.filter = 'brightness(1.1)';
});
bar.addEventListener('mouseleave', () => {
bar.style.filter = 'none';
});
});
</script>
</body></html>

Some files were not shown because too many files have changed in this diff Show More