diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index af5b795..9b0c2b8 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1,4 +1,4 @@ -# Court Lab — UI redesign +# basket-utils — UI redesign 2026-09-07 diff --git a/IMPLEMENTATION-NOTES.md b/IMPLEMENTATION-NOTES.md index a2b013d..fbec7ae 100644 --- a/IMPLEMENTATION-NOTES.md +++ b/IMPLEMENTATION-NOTES.md @@ -1,4 +1,4 @@ -# Court Lab 개선 기록 +# basket-utils 개선 기록 2026-09-07 diff --git a/index.html b/index.html index ef5542d..7b60951 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Court Lab · 농구 전술보드 + basket-utils · 농구 전술보드
diff --git a/server/index.js b/server/index.js index 07dc72a..8c14a1f 100644 --- a/server/index.js +++ b/server/index.js @@ -6,7 +6,7 @@ const { port, host } = config.server; const app = await buildServer({ config }); try { await app.listen({ port, host }); - console.log(`court.lab server listening on http://${host}:${port}`); + console.log(`basket-utils server listening on http://${host}:${port}`); const shutdown = async () => { try { await app.close(); } finally { process.exit(0); } }; process.once('SIGTERM', shutdown); process.once('SIGINT', shutdown); } catch (error) { diff --git a/src/main.js b/src/main.js index ba54ce9..b2d2761 100644 --- a/src/main.js +++ b/src/main.js @@ -70,7 +70,7 @@ function ensureVideoFresh() { if (videoJob && videoJob.key !== videoKey()) videoJob.cancel(); } function videoFileName(name) { - const safe = String(name || 'court-lab-play').trim().replace(/[\\/:*?"<>|]+/g, '-').replace(/\s+/g, '-').slice(0, 70) || 'court-lab-play'; + const safe = String(name || 'basket-utils-play').trim().replace(/[\\/:*?"<>|]+/g, '-').replace(/\s+/g, '-').slice(0, 70) || 'basket-utils-play'; return `${safe}.mp4`; } async function startVideoExport() { @@ -123,7 +123,7 @@ async function startVideoExport() { async function sharePreparedVideo() { ensureVideoFresh(); if (!preparedVideo) { setVideoStatus('먼저 MP4 영상을 만들어 준비하세요'); return; } - try { await shareVideoFile(preparedVideo.file, { navigatorObject: window.navigator, title: state.play.name || 'court.lab 전술 영상', text: '농구 전술 MP4 영상' }); setVideoStatus('공유창을 열었습니다 · 카카오톡 대화방을 선택해 전송하세요'); } + try { await shareVideoFile(preparedVideo.file, { navigatorObject: window.navigator, title: state.play.name || 'basket-utils 전술 영상', text: '농구 전술 MP4 영상' }); setVideoStatus('공유창을 열었습니다 · 카카오톡 대화방을 선택해 전송하세요'); } catch (error) { if (error?.name === 'AbortError') setVideoStatus('영상 파일 공유를 취소했습니다'); else setVideoStatus(error.message || '영상 파일 공유를 지원하지 않습니다'); } } @@ -343,7 +343,7 @@ app.addEventListener('click', (event) => { if (event.target.closest('#apply-position')) { const x = Number(document.querySelector('#action-x').value); const z = Number(document.querySelector('#action-z').value); updatePlay(editAction(state.play, state.selectedSequence, state.selectedPlayerId, state.selectedAction, { location: { x, z } }), '이동 위치 수정', state.selectedAction); return; } if (event.target.closest('#loop')) { const button = document.querySelector('#loop'); const enabled = button.getAttribute('aria-pressed') !== 'true'; button.setAttribute('aria-pressed', String(enabled)); controller.setLoop(enabled); return; } if (event.target.closest('#previous-step') || event.target.closest('#next-step')) { const current = controller.sample().sequenceIndex; const next = Math.max(0, Math.min(state.play.sequences.length - 1, current + (event.target.closest('#next-step') ? 1 : -1))); seekTo(state.play.sequences.slice(0, next).reduce((time, sequence) => time + sequenceDuration(sequence, 4.5, state.play.players), 0)); return; } - if (event.target.closest('#export-play')) { const play = structuredClone(state.play); play.name = document.querySelector('#play-name').value.trim() || '새 전술'; const url = URL.createObjectURL(new Blob([JSON.stringify(play, null, 2)], { type: 'application/json' })); const a = document.createElement('a'); a.href = url; a.download = 'court-lab.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); return; } + if (event.target.closest('#export-play')) { const play = structuredClone(state.play); play.name = document.querySelector('#play-name').value.trim() || '새 전술'; const url = URL.createObjectURL(new Blob([JSON.stringify(play, null, 2)], { type: 'application/json' })); const a = document.createElement('a'); a.href = url; a.download = 'basket-utils.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); return; } if (event.target.closest('#export-video')) { startVideoExport(); return; } if (event.target.closest('#cancel-video')) { videoJob?.cancel(); return; } if (event.target.closest('#share-video')) { sharePreparedVideo(); return; } diff --git a/src/style.css b/src/style.css index 46e6736..a27d3a4 100644 --- a/src/style.css +++ b/src/style.css @@ -8,7 +8,7 @@ input,select { width:100%; min-width:0; min-height:39px; padding:9px 11px; borde .shell { width:100%; height:100dvh; min-height:620px; display:grid; grid-template-rows:76px minmax(0,1fr) 150px; } .topbar { position:relative; z-index:10; display:flex; align-items:center; gap:30px; padding:0 26px; background:white; border-bottom:1px solid var(--line); } .brand { display:flex; align-items:center; gap:10px; flex-shrink:0; width:185px; } .brand-mark { width:37px; height:37px; display:grid; place-items:center; color:#fff4e9; background:var(--accent); border-radius:10px; transform:rotate(-6deg); } .brand-mark .icon { width:26px; height:26px; } -.brand strong { font-size:26px; letter-spacing:-.06em; font-weight:850; line-height:1; } .brand strong span { font-weight:400; } .brand i { font-size:10px; font-style:normal; vertical-align:super; margin-left:3px; } .brand small { display:block; margin-top:5px; font-size:7px; letter-spacing:.14em; color:var(--muted); } +.brand strong { font-size:26px; letter-spacing:-.06em; font-weight:850; line-height:1; white-space:nowrap; } .brand strong span { font-weight:400; } .brand > div { flex-shrink:0; } .brand i { font-size:10px; font-style:normal; vertical-align:super; margin-left:3px; } .brand small { display:block; margin-top:5px; font-size:7px; letter-spacing:.14em; color:var(--muted); } .document-title { display:flex; align-items:center; gap:18px; min-width:0; flex:1; } .breadcrumb { white-space:nowrap; font-size:11px; color:var(--muted); } .breadcrumb span { margin-left:20px; color:#c0c9c1; } #play-name { width:min(250px,100%); background:transparent; border-color:transparent; font-size:14px; font-weight:650; padding-left:0; } #play-name:hover,#play-name:focus { border-color:var(--line); padding-left:10px; } .top-actions { display:flex; align-items:center; gap:10px; } #save-status { max-width:175px; font-size:10px; color:#748579; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-right:7px; } #save-status::before { content:''; display:inline-block; width:5px; height:5px; border-radius:50%; background:#78a085; margin-right:7px; vertical-align:1px; } @@ -52,7 +52,7 @@ h1 { display:flex; align-items:center; gap:10px; color:#f4f2e8; font-size:17px; .playback-deck { padding:0 16px; grid-template-columns:110px minmax(0,1fr); grid-template-rows:minmax(0,1fr) 56px; } .sequence-heading .section-kicker { font-size:7px; } .sequence-heading strong { font-size:11px; } .sequence-tab { height:56px; min-width:105px; padding:5px; } .sequence-tab svg { width:42px; height:42px; } .add-sequence { height:56px; min-width:35px; } .transport { width:110px; gap:3px; } .playback-row { gap:8px; } .timeline { gap:5px; } .timeline output { font-size:9px; } } @media(max-width:520px) { - .shell { grid-template-rows:92px 44px minmax(0,1fr) 134px; min-height:650px; } .topbar { align-items:flex-start; padding:14px 16px 0; gap:6px; } .brand-mark { width:30px; height:30px; border-radius:8px; } .brand-mark .icon { width:22px; height:22px; } .brand strong { font-size:24px; } .document-title { position:absolute; bottom:7px; left:16px; right:16px; } #play-name { width:100%; min-height:28px; font-size:12px; padding-block:4px; } .top-actions { margin-left:auto; gap:6px; } .top-actions button { min-height:32px; padding:6px 9px; font-size:10px; } .top-actions .icon { width:15px; height:15px; } #toggle-menu { width:32px; } .project-menu { top:54px; right:12px; width:min(330px,calc(100vw - 24px)); } + .shell { grid-template-rows:92px 44px minmax(0,1fr) 134px; min-height:650px; } .topbar { align-items:flex-start; padding:14px 16px 0; gap:6px; } .brand { gap:6px; } .brand-mark { width:27px; height:27px; border-radius:8px; } .brand-mark .icon { width:20px; height:20px; } .brand strong { font-size:20px; white-space:nowrap; } .document-title { position:absolute; bottom:7px; left:16px; right:16px; } #play-name { width:100%; min-height:28px; font-size:12px; padding-block:4px; } .top-actions { margin-left:auto; gap:4px; } .top-actions button { min-height:32px; padding:6px 9px; font-size:10px; } .top-actions .icon { width:15px; height:15px; } #toggle-menu { width:32px; } .project-menu { top:54px; right:12px; width:min(330px,calc(100vw - 24px)); } .mobile-tabs { padding:3px 14px; } .mobile-tabs button { flex:1; min-width:0; } .workspace { padding:0 10px 10px; } .workspace .panel { left:10px; right:10px; bottom:10px; padding:18px; } .board-wrap { border-radius:13px; } .canvas-header { top:17px; left:16px; right:16px; } .canvas-header h1 { font-size:14px; } .canvas-header .section-kicker { font-size:7px; } .view-group { padding:3px; } .view-group button { font-size:9px; padding:4px 6px; } .canvas-meta { top:74px; left:16px; right:16px; font-size:7px; } .board { inset:91px 0 98px; } .mode-group { bottom:37px; padding:4px; border-radius:10px; } .mode-group button { min-width:52px; padding:6px 8px; min-height:47px; } .board-status { left:16px; right:16px; bottom:12px; font-size:8px; } .playback-deck { grid-template-columns:74px minmax(0,1fr); padding:0 12px; grid-template-rows:minmax(0,1fr) 58px; } .sequence-heading .section-kicker { font-size:6px; letter-spacing:.07em; } .sequence-heading strong { font-size:10px; } .sequence-tab { min-width:98px; } .transport { width:98px; gap:1px; } .transport .icon-button { width:28px; padding:5px; } .play-button { width:34px; min-height:34px; } .timeline { gap:4px; flex-wrap:wrap; position:relative; padding-top:12px; } .timeline input[type=range] { position:absolute; left:0; right:0; top:1px; width:100%; height:10px; min-height:10px; } .timeline .icon-button { min-height:27px; width:19px; } .timeline output { margin-left:auto; font-size:8px; } .timeline select { width:45px; font-size:9px; min-height:27px; padding:1px; } #loop { min-height:27px; padding:2px 4px; font-size:15px; } #loop span,.shortcut-note { display:none; } } @@ -75,4 +75,4 @@ h1 { display:flex; align-items:center; gap:10px; color:#f4f2e8; font-size:17px; .team-form { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:end; gap:9px; padding-top:18px; border-top:1px solid var(--line); } .team-form button { min-height:39px; } .local-import { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:end; gap:9px; margin-top:13px; } .local-import label { display:grid; gap:7px; color:#627268; font-size:11px; font-weight:600; } .account-secondary { width:100%; margin-top:0; background:#edf2e9; border-color:transparent; color:#496448; font-size:11px; } .account-empty { margin:4px 0; color:var(--muted); font-size:11px; line-height:1.6; } .operator-panel { margin-top:21px; padding-top:17px; border-top:1px solid var(--line); } .operator-heading { display:flex; justify-content:space-between; align-items:center; margin-bottom:11px; } .operator-heading strong { font-size:11px; } .pending-user { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:9px 0; border-bottom:1px solid #eef1eb; color:var(--muted); font-size:10px; } .pending-user button { min-height:29px; padding:5px 9px; font-size:10px; } #open-team-hub { min-height:32px; padding:6px 10px; color:#587160; border-color:#dbe4d8; background:#f5f8f1; font-size:10px; } #team-context { color:#65766b; } -@media(max-width:520px) { .account-gate { align-items:start; padding:14px; } .account-card { margin-top:5vh; padding:24px 20px; border-radius:16px; } .account-brand { margin-bottom:26px; } .team-form,.local-import { grid-template-columns:1fr; } .team-form button,.local-import button { width:100%; } #open-team-hub { width:78px; max-width:78px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .top-actions button { white-space:nowrap; } #video-share-open span,#save-play span { display:none; } #video-share-open,#save-play { width:34px; min-width:34px; padding:6px; } } +@media(max-width:520px) { .account-gate { align-items:start; padding:14px; } .account-card { margin-top:5vh; padding:24px 20px; border-radius:16px; } .account-brand { margin-bottom:26px; } .team-form,.local-import { grid-template-columns:1fr; } .team-form button,.local-import button { width:100%; } #open-team-hub { width:64px; max-width:64px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-inline:4px; } .top-actions button { white-space:nowrap; } #video-share-open span,#save-play span { display:none; } #video-share-open,#save-play { width:34px; min-width:34px; padding:6px; } } diff --git a/src/ui.js b/src/ui.js index 7ea8289..d425641 100644 --- a/src/ui.js +++ b/src/ui.js @@ -20,7 +20,7 @@ export const icon = name => `
-
${icon('court')}
courtlabBASKETBALL PLAY STUDIO
+
${icon('court')}
basket-utilsBASKETBALL PLAY STUDIO
@@ -68,7 +68,7 @@ export function decorateSequenceButton(button, sequence, index, players) { export function editorLayout() { return `
-
${icon('court')}
courtlabBASKETBALL PLAY STUDIO
+
${icon('court')}
basket-utilsBASKETBALL PLAY STUDIO
내 팀 /
저장 전