@@ -20,11 +20,36 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
2020 const [ error , setError ] = useState < string | null > ( null ) ;
2121 const [ message , setMessage ] = useState < string | null > ( null ) ;
2222
23+ // README 2-1: per-image size limit 5MB
24+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024 ;
25+
26+ const describeTooLarge = ( files : File [ ] ) => {
27+ const over = files . filter ( ( f ) => f . size > MAX_IMAGE_BYTES ) ;
28+ if ( over . length === 0 ) return null ;
29+ const names = over
30+ . slice ( 0 , 3 )
31+ . map ( ( f ) => f . name )
32+ . join ( ', ' ) ;
33+ const more = over . length > 3 ? ` 외 ${ over . length - 3 } 개` : '' ;
34+ return `이미지 용량이 너무 커요. (개별 5MB 이하) : ${ names } ${ more } ` ;
35+ } ;
36+
2337 const nowLocal = new Date ( ) ;
2438 const minStartLocal = new Date ( nowLocal . getTime ( ) + 60_000 )
2539 . toISOString ( )
2640 . slice ( 0 , 16 ) ;
2741
42+ const fmtLocal = ( dtLocal : string ) => {
43+ if ( ! dtLocal ) return '' ;
44+ // Convert yyyy-mm-ddThh:mm to a readable label in ko-KR.
45+ try {
46+ const d = new Date ( dtLocal ) ;
47+ return d . toLocaleString ( 'ko-KR' ) ;
48+ } catch {
49+ return dtLocal ;
50+ }
51+ } ;
52+
2853 const addOption = ( ) =>
2954 setOptions ( ( prev ) => [ ...prev , { name : '' , option_image_files : [ ] } ] ) ;
3055 const removeOption = ( idx : number ) =>
@@ -34,17 +59,13 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
3459 prev . map ( ( v , i ) => ( i === idx ? { ...v , name : val } : v ) )
3560 ) ;
3661
37- const addOptionImage = ( idx : number , files : FileList | null ) => {
38- if ( ! files || files . length === 0 ) return ;
62+ const setOptionImage = ( idx : number , file : File | null ) => {
3963 setOptions ( ( prev ) =>
4064 prev . map ( ( v , i ) =>
4165 i === idx
4266 ? {
4367 ...v ,
44- option_image_files : [
45- ...v . option_image_files ,
46- ...Array . from ( files ) ,
47- ] ,
68+ option_image_files : file ? [ file ] : [ ] ,
4869 }
4970 : v
5071 )
@@ -91,11 +112,11 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
91112 }
92113
93114 const optionImageBaseIndex = imageFiles . length ;
94- // Flatten option images in option order (their indices are derived from this order)
115+ // Flatten option images in option order.
116+ // API supports a single option_image_index per option, so only the first selected image is appended.
95117 for ( const opt of options ) {
96- for ( const f of opt . option_image_files ?? [ ] ) {
97- imageFiles . push ( f ) ;
98- }
118+ const first = ( opt . option_image_files ?? [ ] ) [ 0 ] ;
119+ if ( first ) imageFiles . push ( first ) ;
99120 }
100121
101122 // ===== Client-side validation (mirrors README constraints) =====
@@ -151,13 +172,11 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
151172 images : images . length > 0 ? images : undefined ,
152173 options : normalizedOptions . map ( ( o ) => {
153174 // If no image for this option, -1. Otherwise map to shared image_files index.
154- const mapped =
155- o . option_image_files . length > 0 ? runningOptImageIndex : - 1 ;
175+ const hasImage = ( o . option_image_files ?? [ ] ) . length > 0 ;
176+ const mapped = hasImage ? runningOptImageIndex : - 1 ;
156177
157- // Each option can have 0..N images selected, but API supports a single index.
158- // We'll use the first selected image and still upload the rest (harmless).
159- // Advance by the number of files we appended for this option.
160- runningOptImageIndex += o . option_image_files . length ;
178+ // We appended at most 1 file per option above.
179+ if ( hasImage ) runningOptImageIndex += 1 ;
161180
162181 return { name : o . name , option_image_index : mapped } ;
163182 } ) ,
@@ -231,6 +250,10 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
231250 } }
232251 required
233252 />
253+ < p className = "page-sub" style = { { margin : 0 } } >
254+ 현재 시각 기준 1분 이후부터 선택할 수 있어요.
255+ { minStartLocal ? ` (최소: ${ fmtLocal ( minStartLocal ) } )` : '' }
256+ </ p >
234257 </ div >
235258 < div className = "form-row" >
236259 < label htmlFor = "ev-desc" > 설명</ label >
@@ -254,6 +277,9 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
254277 onChange = { ( e ) => setEndAt ( e . target . value ) }
255278 required
256279 />
280+ < p className = "page-sub" style = { { margin : 0 } } >
281+ 시작 시각 이후로만 선택할 수 있어요.
282+ </ p >
257283 </ div >
258284
259285 < div className = "image-input-grid span-2" >
@@ -265,18 +291,30 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
265291 type = "file"
266292 accept = "image/*"
267293 multiple
268- onChange = { ( e ) => setEventImages ( e . target . files ) }
294+ onChange = { ( e ) => {
295+ const files = Array . from ( e . target . files ?? [ ] ) ;
296+ const tooLargeMsg = describeTooLarge ( files ) ;
297+ if ( tooLargeMsg ) {
298+ setError ( tooLargeMsg ) ;
299+ setEventImages ( null ) ;
300+ // reset input so user can reselect
301+ e . currentTarget . value = '' ;
302+ return ;
303+ }
304+ setError ( null ) ;
305+ setEventImages ( e . target . files ) ;
306+ } }
269307 />
270308 < p className = "page-sub" style = { { margin : 0 } } >
271- 선택한 순서대로 image_index가 0부터 부여됩니다 .
309+ 이미지 파일은 개별 5MB 이하만 업로드할 수 있어요 .
272310 </ p >
273311 </ div >
274312
275313 < div className = "form-row" >
276314 < label > 옵션 이미지</ label >
277315 < p className = "page-sub" style = { { margin : 0 } } >
278- 각 옵션 행에서 “이미지 선택”을 눌러 업로드할 이미지를 고르세요.
279- (없으면 이미지 없이 생성됩니다.)
316+ 각 옵션마다 이미지는 1장만 선택할 수 있어요. (없으면 이미지 없이
317+ 생성됩니다.)
280318 </ p >
281319 </ div >
282320 </ div >
@@ -302,16 +340,26 @@ const EventCreateForm = ({ onCreated, onCancel }: Props) => {
302340 accept = "image/*"
303341 style = { { display : 'none' } }
304342 onChange = { ( e ) => {
305- addOptionImage ( idx , e . target . files ) ;
343+ const file = e . target . files ?. [ 0 ] ?? null ;
344+ if ( file ) {
345+ const tooLargeMsg = describeTooLarge ( [ file ] ) ;
346+ if ( tooLargeMsg ) {
347+ setError ( tooLargeMsg ) ;
348+ setOptionImage ( idx , null ) ;
349+ e . currentTarget . value = '' ;
350+ return ;
351+ }
352+ setError ( null ) ;
353+ }
354+
355+ setOptionImage ( idx , file ) ;
306356 // reset input so selecting same file again triggers change
307357 e . currentTarget . value = '' ;
308358 } }
309359 />
310360 </ label >
311361 < small className = "page-sub" style = { { margin : 0 } } >
312- { opt . option_image_files . length > 0
313- ? `${ opt . option_image_files . length } 개 선택됨`
314- : '없음' }
362+ { opt . option_image_files . length > 0 ? '선택됨' : '없음' }
315363 </ small >
316364 { opt . option_image_files . length > 0 ? (
317365 < button
0 commit comments