From 2f649418171d51f69ea3ddc7347c117fab779ea0 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 15 Apr 2026 15:06:45 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=AC=EB=B3=BC=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=EA=B8=B0=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20+=20?= =?UTF-8?q?=EC=B2=B4=EB=82=A9=20=ED=88=B4=ED=8C=81=20+=20=ED=83=AD=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 완료일 계산: 매일 일퀘 + 매 목요일 주간퀘 n회분 일괄 지급으로 시뮬레이션 (extra는 즉시 적용, 금일 일퀘 완료면 오늘 제외) - 각 카드의 남은 일수/예상 완료일, 탭 전체의 완료 예상일 표시 - 주간퀘에 0회(0개) 옵션 추가 - 성장치 호버 시 현재 성장치로 올릴 수 있는 최대 레벨 툴팁 - 선택 탭(아케인/어센틱/그랜드 어센틱)을 캐릭터별로 persist Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/features/symbol/Symbol.jsx | 128 ++++++++++++++++++++---- frontend/src/features/symbol/store.js | 6 ++ 2 files changed, 112 insertions(+), 22 deletions(-) diff --git a/frontend/src/features/symbol/Symbol.jsx b/frontend/src/features/symbol/Symbol.jsx index d836294..5e5a3c5 100644 --- a/frontend/src/features/symbol/Symbol.jsx +++ b/frontend/src/features/symbol/Symbol.jsx @@ -1,11 +1,48 @@ import { useState, useEffect, useMemo } from 'react' import { useQuery, useQueries, useMutation } from '@tanstack/react-query' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' import { api } from '../../api/client' import { useLayout } from '../../components/Layout' import Select from '../../components/Select' import Tooltip from '../../components/Tooltip' import { useSymbolStore } from './store' +dayjs.extend(utc) +dayjs.extend(timezone) +const KST = 'Asia/Seoul' +const DOW = ['일', '월', '화', '수', '목', '금', '토'] + +function formatKoreanDate(d) { + const dj = dayjs(d).tz(KST) + return `${dj.year()}년 ${String(dj.month() + 1).padStart(2, '0')}월 ${String(dj.date()).padStart(2, '0')}일 (${DOW[dj.day()]})` +} + +/** + * 심볼 완료까지 남은 일수/예상 완료일 계산 + * - 일퀘는 매일, 주간퀘는 매주 목요일 리셋 시 N회분을 한 번에 지급한다고 가정 + * - extra(추가 심볼)는 즉시 적용 + * - dailyDone이면 오늘 일퀘는 이미 받은 걸로 간주 (내일부터 다시 지급) + */ +function computeCompletion({ remainingSymbols, daily, weeklyPerWeek, extra, dailyDone }) { + const need = Math.max(remainingSymbols - extra, 0) + if (need === 0) return { days: 0, date: dayjs().tz(KST).startOf('day').toDate() } + if (daily <= 0 && weeklyPerWeek <= 0) return { days: null, date: null } + + let acc = 0 + let cursor = dayjs().tz(KST).startOf('day') + for (let day = 0; day < 3650; day++) { + // 오늘은 dailyDone이면 일퀘 없음, 그 외엔 daily + if (!(day === 0 && dailyDone)) acc += daily + // 목요일(day=4)에 주간퀘 전량 지급 + if (cursor.day() === 4 && weeklyPerWeek > 0) acc += weeklyPerWeek + if (acc >= need) return { days: day, date: cursor.toDate() } + cursor = cursor.add(1, 'day') + } + return { days: null, date: null } +} + function formatMesoKorean(n) { const v = Number(n) || 0 if (v <= 0) return '0' @@ -105,8 +142,31 @@ function SymbolCard({ symbol, equipped, charId }) { return { remainingSymbols: sym, remainingMeso: meso, arrearMeso: arr } }, [equipped, level, growth, symbol.levels]) - const daysLeft = '-' - const completeDate = '-' + // 현재 성장치로 도달 가능한 최대 레벨 (연속 체납 반영) + const reachableLevel = useMemo(() => { + if (!equipped || isMax) return level + let lv = level + let g = growth + while (lv < symbol.max_level) { + const req = symbol.levels?.find((l) => l.level === lv)?.required_count + if (!req || g < req) break + g -= req + lv += 1 + } + return lv + }, [equipped, isMax, level, growth, symbol.levels, symbol.max_level]) + + // 남은 일수/예상 완료일 + const { days: daysLeft, date: completeDate } = useMemo(() => { + if (!equipped || isMax) return { days: null, date: null } + return computeCompletion({ + remainingSymbols, + daily, + weeklyPerWeek: (weeklyCount || 0) * (symbol.weekly_default || 0), + extra, + dailyDone, + }) + }, [equipped, isMax, remainingSymbols, daily, weeklyCount, symbol.weekly_default, extra, dailyDone]) return (
- - 성장치 {isMax ? ( - MAX - ) : ( - <>{growth} / {requireGrowth} - )} - + {reachableLevel > level ? ( + + + 성장치 {growth} / {requireGrowth} + + + ) : ( + + 성장치 {isMax ? ( + MAX + ) : ( + <>{growth} / {requireGrowth} + )} + + )} {!isMax && ( {requireGrowth ? Math.min(Math.floor((growth / requireGrowth) * 100), 100) : 0}% @@ -195,7 +263,7 @@ function SymbolCard({ symbol, equipped, charId }) {