@@ -3,22 +3,88 @@ import { useNavigate } from 'react-router-dom';
33import iconFaceChef from '../assets/icon-face-chef.png' ;
44import { BackHeader } from '../components/BackHeader' ;
55import { PrimaryButton } from '../components/PrimaryButton' ;
6- import { getRecipe , type Recipe } from '../lib/recipes ' ;
7- import { getSelectedRecipe } from '../lib/storage' ;
6+ import { recommend , type RecipeDetailDto } from '../lib/api ' ;
7+ import { getAllergies , getIngredients , getSelectedRecipe , getSelectedRecipeId } from '../lib/storage' ;
88import { colors , shadow } from '../theme' ;
99
1010export function RecipeDetailPage ( ) {
1111 const navigate = useNavigate ( ) ;
12- const [ recipeName , setRecipeName ] = useState ( '두부계란덮밥' ) ;
13- const [ recipe , setRecipe ] = useState < Recipe | null > ( null ) ;
12+ const [ recipeName , setRecipeName ] = useState ( getSelectedRecipe ( ) ) ;
13+ const [ detail , setDetail ] = useState < RecipeDetailDto | null > ( null ) ;
14+ const [ phase , setPhase ] = useState < 'loading' | 'done' | 'error' > ( 'loading' ) ;
15+ const [ error , setError ] = useState ( '' ) ;
1416
1517 useEffect ( ( ) => {
16- const name = getSelectedRecipe ( ) ;
17- setRecipeName ( name ) ;
18- setRecipe ( getRecipe ( name ) ) ;
18+ const ingredientIds = getIngredients ( ) . map ( ( i ) => i . id ) ;
19+ const allergenIds = getAllergies ( ) . selected . map ( ( a ) => a . id ) ;
20+ const recipeId = getSelectedRecipeId ( ) ;
21+
22+ recommend ( ingredientIds , allergenIds , recipeId )
23+ . then ( ( result ) => {
24+ if ( ! result . detail ) {
25+ throw new Error ( '레시피 상세 정보를 불러오지 못했어요.' ) ;
26+ }
27+ setDetail ( result . detail ) ;
28+ setRecipeName ( result . detail . name ) ;
29+ setPhase ( 'done' ) ;
30+ } )
31+ . catch ( ( err ) => {
32+ setError ( err instanceof Error ? err . message : '레시피 상세 정보를 불러오지 못했어요.' ) ;
33+ setPhase ( 'error' ) ;
34+ } ) ;
1935 } , [ ] ) ;
2036
21- const steps = recipe ?. steps ?? [ ] ;
37+ const steps = detail ?. cooking_steps ?? [ ] ;
38+ const requiredIngredients = detail ?. classification ?. required ?? detail ?. missing_ingredients ?? [ ] ;
39+ const optionalIngredients = detail ?. classification ?. optional ?? [ ] ;
40+
41+ if ( phase === 'loading' ) {
42+ return (
43+ < div style = { { minHeight : '100vh' , display : 'flex' , flexDirection : 'column' } } >
44+ < BackHeader title = "레시피 상세" onBack = { ( ) => navigate ( '/recipes/loading' ) } maxWidth = { 560 } />
45+ < div
46+ style = { {
47+ flex : 1 ,
48+ display : 'flex' ,
49+ alignItems : 'center' ,
50+ justifyContent : 'center' ,
51+ fontSize : 14 ,
52+ color : colors . textMuted ,
53+ } }
54+ >
55+ 레시피 정보를 불러오는 중이에요..
56+ </ div >
57+ </ div >
58+ ) ;
59+ }
60+
61+ if ( phase === 'error' ) {
62+ return (
63+ < div style = { { minHeight : '100vh' , display : 'flex' , flexDirection : 'column' } } >
64+ < BackHeader title = "레시피 상세" onBack = { ( ) => navigate ( '/recipes/loading' ) } maxWidth = { 560 } />
65+ < div
66+ style = { {
67+ flex : 1 ,
68+ display : 'flex' ,
69+ flexDirection : 'column' ,
70+ alignItems : 'center' ,
71+ justifyContent : 'center' ,
72+ gap : 12 ,
73+ padding : '0 24px' ,
74+ textAlign : 'center' ,
75+ } }
76+ >
77+ < div style = { { fontSize : 15 , color : colors . allergyText , wordBreak : 'keep-all' } } > { error } </ div >
78+ < div
79+ onClick = { ( ) => navigate ( '/recipes/loading' ) }
80+ style = { { fontSize : 13 , fontWeight : 800 , color : colors . teal , cursor : 'pointer' } }
81+ >
82+ 다시 시도하기
83+ </ div >
84+ </ div >
85+ </ div >
86+ ) ;
87+ }
2288
2389 return (
2490 < div style = { { minHeight : '100vh' , display : 'flex' , flexDirection : 'column' } } >
@@ -68,7 +134,7 @@ export function RecipeDetailPage() {
68134 borderRadius : 999 ,
69135 } }
70136 >
71- < span > ⏱ { recipe ?. time } </ span >
137+ < span > ⏱ { detail ?. cooking_time != null ? ` ${ detail . cooking_time } 분` : '-' } </ span >
72138 < span > ·</ span >
73139 < span > { steps . length } 단계</ span >
74140 </ div >
@@ -86,8 +152,8 @@ export function RecipeDetailPage() {
86152 < div style = { { fontSize : 13 , fontWeight : 700 , color : colors . navy , marginBottom : 12 } } >
87153 필요한 재료
88154 </ div >
89- < div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 } } >
90- { ( recipe ?. ingredients ?? [ ] ) . map ( ( item ) => (
155+ < div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 , marginBottom : optionalIngredients . length ? 16 : 0 } } >
156+ { requiredIngredients . map ( ( item ) => (
91157 < div
92158 key = { item }
93159 style = { {
@@ -103,6 +169,30 @@ export function RecipeDetailPage() {
103169 </ div >
104170 ) ) }
105171 </ div >
172+ { optionalIngredients . length > 0 && (
173+ < >
174+ < div style = { { fontSize : 13 , fontWeight : 700 , color : colors . navy , marginBottom : 12 } } >
175+ 생략 가능한 재료
176+ </ div >
177+ < div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 } } >
178+ { optionalIngredients . map ( ( item ) => (
179+ < div
180+ key = { item }
181+ style = { {
182+ padding : '8px 14px' ,
183+ borderRadius : 999 ,
184+ background : colors . bg ,
185+ color : colors . textMuted ,
186+ fontSize : 13 ,
187+ fontWeight : 700 ,
188+ } }
189+ >
190+ { item }
191+ </ div >
192+ ) ) }
193+ </ div >
194+ </ >
195+ ) }
106196 </ div >
107197
108198 < div
@@ -145,7 +235,9 @@ export function RecipeDetailPage() {
145235 </ div >
146236 </ div >
147237
148- < PrimaryButton onClick = { ( ) => navigate ( '/cooking/steps' ) } >
238+ < PrimaryButton
239+ onClick = { ( ) => navigate ( '/cooking/steps' , { state : { steps, name : recipeName } } ) }
240+ >
149241 요리 시작하기
150242 </ PrimaryButton >
151243 </ div >
0 commit comments