feat: 튜토리얼 data-tutorial 마커 및 TutorialOverlay 앱 통합 (JSA-47)

- ElementsScreen: 첫 번째 obtained 원소 카드에 data-tutorial 스포트라이트 마커
- FusionScreen: 합성 슬롯1, 합성 결과 배너에 data-tutorial 마커
- EvolutionScreen: 첫 번째 강화 버튼에 data-tutorial 마커
- _app.tsx: AppContainer에 <TutorialOverlay /> 전역 마운트

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-01 22:44:17 +09:00
parent ff0ed541cd
commit 3178d880f2
4 changed files with 20 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ import { PropsWithChildren } from 'react';
import { InitialProps } from '@granite-js/react-native';
import { context } from '../require.context';
import { initAnalytics } from './analytics';
import { TutorialOverlay } from './components/tutorial/TutorialOverlay';
initAnalytics(process.env.NODE_ENV !== 'production');
@@ -11,6 +12,7 @@ function AppContainer({ children }: PropsWithChildren<InitialProps>) {
return (
<TDSMobileAITProvider brandPrimaryColor="#FF6B35">
{children}
<TutorialOverlay />
</TDSMobileAITProvider>
);
}

View File

@@ -469,9 +469,10 @@ interface ElementCardProps {
count: number;
level: number;
onSelect: (el: ElementData) => void;
isTutorialTarget?: boolean;
}
const ElementCard = memo(function ElementCard({ el, state, count, level, onSelect }: ElementCardProps) {
const ElementCard = memo(function ElementCard({ el, state, count, level, onSelect, isTutorialTarget }: ElementCardProps) {
const spawnRate = calcSpawnRate(el.id, level);
if (state === 'undiscovered') {
@@ -498,7 +499,11 @@ const ElementCard = memo(function ElementCard({ el, state, count, level, onSelec
}
return (
<div css={obtainedCardStyle} onClick={() => onSelect(el)}>
<div
css={obtainedCardStyle}
onClick={() => onSelect(el)}
data-tutorial={isTutorialTarget ? 'elements-first-card' : undefined}
>
<div css={spriteWrapStyle}>
<CharacterSprite elementId={el.id} elementColor={el.color} tier={el.tier} size={56} state="obtained" />
</div>
@@ -593,6 +598,9 @@ export function ElementsScreen() {
items: elementsData.filter((el) => el.tier === tier),
}));
// 첫 번째 obtained 원소 ID (튜토리얼 스포트라이트용)
const firstObtainedId = obtainedElements[0]?.id ?? null;
const unlockRatio = obtainedElements.length / totalElements;
return (
@@ -644,6 +652,7 @@ export function ElementsScreen() {
count={elements[el.id] ?? 0}
level={elementLevels[el.id] ?? 0}
onSelect={setSelectedEl}
isTutorialTarget={el.id === firstObtainedId}
/>
))}
</div>

View File

@@ -220,7 +220,7 @@ export function EvolutionScreen() {
<div css={goldBadgeStyle}>💰 {gold}</div>
</div>
{ownedElements.map((el) => {
{ownedElements.map((el, index) => {
const level = elementLevels[el.id] ?? 0;
const isMax = level >= ENHANCE_MAX_LEVEL;
const cost = isMax ? 0 : calcEnhanceCost(level);
@@ -257,6 +257,7 @@ export function EvolutionScreen() {
css={enhanceButtonStyle(canEnhance)}
onClick={(e) => handleEnhance(el.id, e.currentTarget)}
disabled={!canEnhance}
data-tutorial={index === 0 ? 'elements-enhance-button' : undefined}
>
{isMax
? '✅ 최대 레벨 달성'

View File

@@ -502,6 +502,7 @@ export function FusionScreen() {
<div
css={slotStyle(!!slot1, selectingSlot === 1)}
onClick={() => handleSlotClick(1)}
data-tutorial="fusion-slot1"
>
{slot1 ? (
<>
@@ -546,7 +547,10 @@ export function FusionScreen() {
{/* 합성 결과 피드백 */}
{lastResult?.type === 'success' && lastResult.resultId && (
<div css={resultBannerStyle(elementMap[lastResult.resultId]?.rarity ?? 'common')}>
<div
css={resultBannerStyle(elementMap[lastResult.resultId]?.rarity ?? 'common')}
data-tutorial="fusion-result"
>
<span css={resultEmojiStyle(elementMap[lastResult.resultId]?.rarity ?? 'common')}>
{elementMap[lastResult.resultId]?.emoji}
</span>