@@ -3,7 +3,20 @@ import { useBackendAdminClient, useBulkUpdatePageSectionsMutation, useListPageSe
33import { useCommonContext } from "@frontend/common/hooks/useCommonContext" ;
44import { PageSectionSchema } from "@frontend/common/schemas/backendAdminAPI" ;
55import { Add , Delete , OpenInNew } from "@mui/icons-material" ;
6- import { Box , Button , ButtonProps , CircularProgress , Divider , Stack , Tab , Tabs , ThemeProvider } from "@mui/material" ;
6+ import {
7+ Box ,
8+ Button ,
9+ ButtonProps ,
10+ CircularProgress ,
11+ Divider ,
12+ IconButton ,
13+ Stack ,
14+ Tab ,
15+ Tabs ,
16+ TextField ,
17+ ThemeProvider ,
18+ Typography ,
19+ } from "@mui/material" ;
720import { ErrorBoundary , Suspense } from "@suspensive/react" ;
821import { commands } from "@uiw/react-md-editor" ;
922import { FC , SyntheticEvent , useState } from "react" ;
@@ -16,6 +29,10 @@ import { addErrorSnackbar } from "@apps/pyconkr-admin/utils/snackbar";
1629
1730type SectionType = PageSectionSchema ;
1831
32+ // 섹션 편집 탭. "css"는 dynamic_route에서 parseCss로 파싱되는 React 스타일 객체(JSON 문자열)를 편집함.
33+ type SectionTab = "ko" | "en" | "css" ;
34+ const SECTION_TABS : SectionTab [ ] = [ "ko" , "en" , "css" ] ;
35+
1936type CommonSectionEditorPropType = {
2037 disabled ?: boolean ;
2138 onInsertNewSection : ( ) => void ;
@@ -27,8 +44,13 @@ type SectionTextEditorPropType = CommonSectionEditorPropType & {
2744 onChange : ( value ?: string ) => void ;
2845} ;
2946
47+ type SectionCssEditorPropType = CommonSectionEditorPropType & {
48+ defaultValue ?: string | null ;
49+ onChange : ( value : string ) => void ;
50+ } ;
51+
3052type SectionEditorPropType = CommonSectionEditorPropType & {
31- language : "ko" | "en" ;
53+ tab : SectionTab ;
3254 defaultValue : SectionType ;
3355 onChange : ( value : SectionType ) => void ;
3456} ;
@@ -60,18 +82,72 @@ const SectionTextEditor: FC<SectionTextEditorPropType> = ({ disabled, defaultVal
6082 ) ;
6183} ;
6284
63- const SectionEditorField : FC < SectionEditorPropType > = ( { language, disabled, defaultValue, onInsertNewSection, onChange, onDelete } ) => {
64- const onFieldChange = ( key : "body_ko" | "body_en" , value ?: string ) => onChange ( { ...defaultValue , [ key ] : value } ) ;
85+ // 빈 문자열은 유효(스타일 없음)로 간주하고, 유효하지 않으면 파싱 에러 메시지를 반환.
86+ const getJsonError = ( value : string ) : string | null => {
87+ if ( ! value . trim ( ) ) return null ;
88+ try {
89+ JSON . parse ( value ) ;
90+ return null ;
91+ } catch ( e ) {
92+ return ( e as Error ) . message ;
93+ }
94+ } ;
95+
96+ const SectionCssEditor : FC < SectionCssEditorPropType > = ( { disabled, defaultValue, onInsertNewSection, onChange, onDelete } ) => {
97+ const value = defaultValue ?? "" ;
98+ const jsonError = getJsonError ( value ) ;
6599
66100 return (
67- < Stack direction = "row" sx = { { flexGrow : 1 , width : "100%" , height : "100%" , maxWidth : "100%" } } >
101+ < Stack spacing = { 1 } sx = { { flexGrow : 1 , width : "100%" , height : "100%" , maxWidth : "100%" } } >
102+ < Stack direction = "row" justifyContent = "space-between" alignItems = "center" >
103+ < Typography variant = "subtitle2" color = "text.secondary" >
104+ 섹션 CSS (React 스타일 객체 · JSON 형식)
105+ </ Typography >
106+ < IconButton size = "small" onClick = { onDelete } disabled = { disabled } aria-label = "Delete" >
107+ < Delete style = { { fontSize : 16 } } />
108+ </ IconButton >
109+ </ Stack >
110+ < TextField
111+ multiline
112+ minRows = { 8 }
113+ fullWidth
114+ disabled = { disabled }
115+ value = { value }
116+ onChange = { ( e ) => onChange ( e . target . value ) }
117+ error = { Boolean ( jsonError ) }
118+ helperText = { jsonError ? `JSON 파싱 오류: ${ jsonError } ` : '예: {"backgroundColor": "#ffffff", "padding": "16px"}' }
119+ slotProps = { { input : { sx : { fontFamily : "monospace" , fontSize : 13 , alignItems : "flex-start" } } } }
120+ />
121+ < Button size = "small" onClick = { onInsertNewSection } startIcon = { < Add /> } >
122+ 여기에 섹션 추가
123+ </ Button >
124+ </ Stack >
125+ ) ;
126+ } ;
127+
128+ const SectionEditorField : FC < SectionEditorPropType > = ( { tab, disabled, defaultValue, onInsertNewSection, onChange, onDelete } ) => {
129+ const child =
130+ tab === "css" ? (
131+ < SectionCssEditor
132+ disabled = { disabled }
133+ onInsertNewSection = { onInsertNewSection }
134+ onDelete = { onDelete }
135+ defaultValue = { defaultValue ?. css }
136+ onChange = { ( css ) => onChange ( { ...defaultValue , css } ) }
137+ />
138+ ) : (
68139 < SectionTextEditor
69140 disabled = { disabled }
70141 onInsertNewSection = { onInsertNewSection }
71142 onDelete = { onDelete }
72- defaultValue = { defaultValue ?. [ `body_${ language } ` ] || undefined }
73- onChange = { ( text ) => onFieldChange ( `body_${ language } ` , text ) }
143+ defaultValue = { defaultValue ?. [ `body_${ tab } ` ] || undefined }
144+ onChange = { ( text ) => onChange ( { ... defaultValue , [ `body_${ tab } ` ] : text } ) }
74145 />
146+ ) ;
147+
148+ return (
149+ < Stack direction = "row" sx = { { flexGrow : 1 , width : "100%" , height : "100%" , maxWidth : "100%" } } >
150+ { child }
75151 </ Stack >
76152 ) ;
77153} ;
@@ -154,6 +230,7 @@ export const AdminCMSPageEditor: FC = ErrorBoundary.with(
154230 < Tabs orientation = "vertical" value = { editorState . tab } onChange = { setTab } scrollButtons = { false } >
155231 < Tab wrapped label = "한국어" />
156232 < Tab wrapped label = "영어" />
233+ < Tab wrapped label = "CSS" />
157234 </ Tabs >
158235 < Stack sx = { { width : "100%" , height : "100%" , maxWidth : "100%" } } >
159236 < Button size = "small" onClick = { insertNewSection ( 0 ) } startIcon = { < Add /> } >
@@ -163,7 +240,7 @@ export const AdminCMSPageEditor: FC = ErrorBoundary.with(
163240 < SectionEditorField
164241 key = { section . id || index }
165242 defaultValue = { section }
166- language = { editorState . tab === 0 ? "ko" : "en" }
243+ tab = { SECTION_TABS [ editorState . tab ] }
167244 onInsertNewSection = { insertNewSection ( index + 1 ) }
168245 onChange = { onSectionDataChange ( index ) }
169246 onDelete = { deleteSection ( index ) }
0 commit comments