podo-ui 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,13 +7,15 @@ import Chip from './react/atom/chip';
7
7
  import Tooltip from './react/atom/tooltip';
8
8
  import Pagination from './react/molecule/pagination';
9
9
  import Field from './react/molecule/field';
10
+ import DatePicker from './react/molecule/datepicker';
10
11
  declare const Form: {
11
12
  Input: import("react").FC<import("./react/atom/input").InputWrapperProps>;
12
13
  Textarea: import("react").FC<import("./react/atom/textarea").TextareaWrapperProps>;
13
14
  Editor: ({ value, width, height, minHeight, maxHeight, resizable, onChange, validator, placeholder, toolbar, }: import("./react/atom/editor").EditorProps) => import("react/jsx-runtime").JSX.Element;
14
15
  EditorView: ({ value, className }: import("./react/atom/editor-view").EditorViewProps) => import("react/jsx-runtime").JSX.Element;
15
16
  Field: ({ label, labelClass, required, helper, helperClass, children, validator, value, setClassName, className, }: import("./react/molecule/field").FieldProps) => import("react/jsx-runtime").JSX.Element;
17
+ DatePicker: import("react").FC<import("./react/molecule/datepicker").DatePickerProps>;
16
18
  };
17
19
  export default Form;
18
- export { Input, Textarea, Editor, EditorView, Avatar, Chip, Tooltip, Pagination, Field };
20
+ export { Input, Textarea, Editor, EditorView, Avatar, Chip, Tooltip, Pagination, Field, DatePicker };
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,oBAAoB,CAAC;AACvC,OAAO,QAAQ,MAAM,uBAAuB,CAAC;AAC7C,OAAO,MAAM,MAAM,qBAAqB,CAAC;AACzC,OAAO,UAAU,MAAM,0BAA0B,CAAC;AAClD,OAAO,MAAM,MAAM,qBAAqB,CAAC;AACzC,OAAO,IAAI,MAAM,mBAAmB,CAAC;AACrC,OAAO,OAAO,MAAM,sBAAsB,CAAC;AAC3C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,KAAK,MAAM,wBAAwB,CAAC;AAC3C,QAAA,MAAM,IAAI;;;;;;CAMT,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,oBAAoB,CAAC;AACvC,OAAO,QAAQ,MAAM,uBAAuB,CAAC;AAC7C,OAAO,MAAM,MAAM,qBAAqB,CAAC;AACzC,OAAO,UAAU,MAAM,0BAA0B,CAAC;AAClD,OAAO,MAAM,MAAM,qBAAqB,CAAC;AACzC,OAAO,IAAI,MAAM,mBAAmB,CAAC;AACrC,OAAO,OAAO,MAAM,sBAAsB,CAAC;AAC3C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,KAAK,MAAM,wBAAwB,CAAC;AAC3C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,QAAA,MAAM,IAAI;;;;;;;CAOT,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -7,12 +7,14 @@ import Chip from './react/atom/chip';
7
7
  import Tooltip from './react/atom/tooltip';
8
8
  import Pagination from './react/molecule/pagination';
9
9
  import Field from './react/molecule/field';
10
+ import DatePicker from './react/molecule/datepicker';
10
11
  const Form = {
11
12
  Input,
12
13
  Textarea,
13
14
  Editor,
14
15
  EditorView,
15
16
  Field,
17
+ DatePicker,
16
18
  };
17
19
  export default Form;
18
- export { Input, Textarea, Editor, EditorView, Avatar, Chip, Tooltip, Pagination, Field };
20
+ export { Input, Textarea, Editor, EditorView, Avatar, Chip, Tooltip, Pagination, Field, DatePicker };
@@ -0,0 +1,64 @@
1
+ export type DatePickerMode = 'instant' | 'period';
2
+ export type DatePickerType = 'date' | 'time' | 'datetime';
3
+ /** 시간 값 (시, 분) */
4
+ export interface TimeValue {
5
+ hour: number;
6
+ minute: number;
7
+ }
8
+ export interface DatePickerValue {
9
+ /** 시작 날짜 (년, 월, 일) */
10
+ date?: Date;
11
+ /** 시작 시간 (시, 분) */
12
+ time?: TimeValue;
13
+ /** 종료 날짜 (년, 월, 일) - period 모드에서 사용 */
14
+ endDate?: Date;
15
+ /** 종료 시간 (시, 분) - period 모드에서 사용 */
16
+ endTime?: TimeValue;
17
+ }
18
+ /** 날짜 범위 정의 */
19
+ export interface DateRange {
20
+ from: Date;
21
+ to: Date;
22
+ }
23
+ /** 날짜 비활성화/활성화 조건 타입 */
24
+ export type DateCondition = Date | DateRange | ((date: Date) => boolean);
25
+ /** 날짜+시간 제한 값 (날짜만 또는 날짜+시간) */
26
+ export interface DateTimeLimit {
27
+ date: Date;
28
+ time?: TimeValue;
29
+ }
30
+ /** 분 단위 선택 옵션 */
31
+ export type MinuteStep = 1 | 5 | 10 | 15 | 20 | 30;
32
+ export interface DatePickerProps {
33
+ /** 선택 모드: instant(단일) | period(기간) */
34
+ mode?: DatePickerMode;
35
+ /** 값 타입: date | time | datetime */
36
+ type?: DatePickerType;
37
+ /** 선택된 값 */
38
+ value?: DatePickerValue;
39
+ /** 값 변경 콜백 */
40
+ onChange?: (value: DatePickerValue) => void;
41
+ /** 플레이스홀더 */
42
+ placeholder?: string;
43
+ /** 비활성화 */
44
+ disabled?: boolean;
45
+ /** 하단 버튼 표시 (mode가 period일 때 기본 true) */
46
+ showActions?: boolean;
47
+ /** 드롭다운 정렬 */
48
+ align?: 'left' | 'right';
49
+ /** 클래스명 */
50
+ className?: string;
51
+ /** 선택 불가능한 날짜 조건 (특정 날짜, 범위, 함수) */
52
+ disable?: DateCondition[];
53
+ /** 선택 가능한 날짜 조건 (지정된 날짜만 활성화) */
54
+ enable?: DateCondition[];
55
+ /** 선택 가능한 최소 날짜 (Date 또는 { date, time }) */
56
+ minDate?: Date | DateTimeLimit;
57
+ /** 선택 가능한 최대 날짜 (Date 또는 { date, time }) */
58
+ maxDate?: Date | DateTimeLimit;
59
+ /** 분 단위 선택 간격 (1, 5, 10, 15, 30) 기본값: 1 */
60
+ minuteStep?: MinuteStep;
61
+ }
62
+ declare const DatePicker: React.FC<DatePickerProps>;
63
+ export default DatePicker;
64
+ //# sourceMappingURL=datepicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datepicker.d.ts","sourceRoot":"","sources":["../../../react/molecule/datepicker.tsx"],"names":[],"mappings":"AAMA,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAClD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D,kBAAkB;AAClB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,mBAAmB;IACnB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,uCAAuC;IACvC,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,oCAAoC;IACpC,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB;AAED,eAAe;AACf,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,IAAI,CAAC;CACV;AAED,wBAAwB;AACxB,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC;AAEzE,gCAAgC;AAChC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,iBAAiB;AACjB,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,YAAY;IACZ,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,cAAc;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,aAAa;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yCAAyC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,WAAW;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,iCAAiC;IACjC,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;IACzB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,IAAI,GAAG,aAAa,CAAC;IAC/B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,IAAI,GAAG,aAAa,CAAC;IAC/B,2CAA2C;IAC3C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAygBD,QAAA,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAmiBzC,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -0,0 +1,593 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import styles from './datepicker.module.scss';
5
+ // Helper functions
6
+ const formatDate = (date) => {
7
+ const year = date.getFullYear();
8
+ const month = String(date.getMonth() + 1).padStart(2, '0');
9
+ const day = String(date.getDate()).padStart(2, '0');
10
+ return `${year} - ${month} - ${day}`;
11
+ };
12
+ const formatTime = (date) => {
13
+ const hours = String(date.getHours()).padStart(2, '0');
14
+ const minutes = String(date.getMinutes()).padStart(2, '0');
15
+ return `${hours} : ${minutes}`;
16
+ };
17
+ const formatDateTime = (date) => {
18
+ return `${formatDate(date)} ${formatTime(date)}`;
19
+ };
20
+ const isSameDay = (date1, date2) => {
21
+ return (date1.getFullYear() === date2.getFullYear() &&
22
+ date1.getMonth() === date2.getMonth() &&
23
+ date1.getDate() === date2.getDate());
24
+ };
25
+ const isInRange = (date, start, end) => {
26
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
27
+ const s = new Date(start.getFullYear(), start.getMonth(), start.getDate());
28
+ const e = new Date(end.getFullYear(), end.getMonth(), end.getDate());
29
+ return d >= s && d <= e;
30
+ };
31
+ const isInRangeExclusive = (date, start, end) => {
32
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
33
+ const s = new Date(start.getFullYear(), start.getMonth(), start.getDate());
34
+ const e = new Date(end.getFullYear(), end.getMonth(), end.getDate());
35
+ return d > s && d < e;
36
+ };
37
+ const getDaysInMonth = (year, month) => {
38
+ return new Date(year, month + 1, 0).getDate();
39
+ };
40
+ const getFirstDayOfMonth = (year, month) => {
41
+ return new Date(year, month, 1).getDay();
42
+ };
43
+ /** DateRange 타입 가드 */
44
+ const isDateRange = (condition) => {
45
+ return typeof condition === 'object' && 'from' in condition && 'to' in condition;
46
+ };
47
+ /** 날짜가 조건에 해당하는지 확인 */
48
+ const matchesCondition = (date, condition) => {
49
+ if (typeof condition === 'function') {
50
+ return condition(date);
51
+ }
52
+ if (isDateRange(condition)) {
53
+ return isInRange(date, condition.from, condition.to);
54
+ }
55
+ // Date 타입
56
+ return isSameDay(date, condition);
57
+ };
58
+ /** 날짜가 비활성화되어야 하는지 확인 */
59
+ const isDateDisabled = (date, disable, enable) => {
60
+ // enable이 지정된 경우: 지정된 조건에 맞지 않으면 비활성화
61
+ if (enable && enable.length > 0) {
62
+ const isEnabled = enable.some((condition) => matchesCondition(date, condition));
63
+ return !isEnabled;
64
+ }
65
+ // disable이 지정된 경우: 지정된 조건에 맞으면 비활성화
66
+ if (disable && disable.length > 0) {
67
+ return disable.some((condition) => matchesCondition(date, condition));
68
+ }
69
+ return false;
70
+ };
71
+ /** DateTimeLimit 타입 가드 */
72
+ const isDateTimeLimit = (value) => {
73
+ return typeof value === 'object' && 'date' in value && !(value instanceof Date);
74
+ };
75
+ /** DateTimeLimit에서 Date와 TimeValue 추출 */
76
+ const extractDateTimeLimit = (limit) => {
77
+ if (isDateTimeLimit(limit)) {
78
+ return { date: limit.date, time: limit.time };
79
+ }
80
+ return { date: limit };
81
+ };
82
+ /** 날짜가 minDate보다 이전인지 확인 (날짜만 비교) */
83
+ const isBeforeMinDate = (date, minDate) => {
84
+ if (!minDate)
85
+ return false;
86
+ const { date: minDateValue } = extractDateTimeLimit(minDate);
87
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
88
+ const m = new Date(minDateValue.getFullYear(), minDateValue.getMonth(), minDateValue.getDate());
89
+ return d < m;
90
+ };
91
+ /** 날짜가 maxDate보다 이후인지 확인 (날짜만 비교) */
92
+ const isAfterMaxDate = (date, maxDate) => {
93
+ if (!maxDate)
94
+ return false;
95
+ const { date: maxDateValue } = extractDateTimeLimit(maxDate);
96
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
97
+ const m = new Date(maxDateValue.getFullYear(), maxDateValue.getMonth(), maxDateValue.getDate());
98
+ return d > m;
99
+ };
100
+ /** 날짜+시간이 minDate보다 이전인지 확인 */
101
+ const isBeforeMinDateTime = (date, time, minDate) => {
102
+ if (!minDate)
103
+ return false;
104
+ const { date: minDateValue, time: minTime } = extractDateTimeLimit(minDate);
105
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
106
+ const m = new Date(minDateValue.getFullYear(), minDateValue.getMonth(), minDateValue.getDate());
107
+ if (d < m)
108
+ return true;
109
+ if (d > m)
110
+ return false;
111
+ // 같은 날짜: 시간 비교
112
+ if (minTime && time) {
113
+ if (time.hour < minTime.hour)
114
+ return true;
115
+ if (time.hour === minTime.hour && time.minute < minTime.minute)
116
+ return true;
117
+ }
118
+ return false;
119
+ };
120
+ /** 날짜+시간이 maxDate보다 이후인지 확인 */
121
+ const isAfterMaxDateTime = (date, time, maxDate) => {
122
+ if (!maxDate)
123
+ return false;
124
+ const { date: maxDateValue, time: maxTime } = extractDateTimeLimit(maxDate);
125
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
126
+ const m = new Date(maxDateValue.getFullYear(), maxDateValue.getMonth(), maxDateValue.getDate());
127
+ if (d > m)
128
+ return true;
129
+ if (d < m)
130
+ return false;
131
+ // 같은 날짜: 시간 비교
132
+ if (maxTime && time) {
133
+ if (time.hour > maxTime.hour)
134
+ return true;
135
+ if (time.hour === maxTime.hour && time.minute > maxTime.minute)
136
+ return true;
137
+ }
138
+ return false;
139
+ };
140
+ const Calendar = ({ value, endValue, mode, onSelect, viewDate, onViewDateChange, showPrevNav = true, showNextNav = true, minViewDate, maxViewDate, disable, enable, minDate, maxDate, }) => {
141
+ const today = new Date();
142
+ const year = viewDate.getFullYear();
143
+ const month = viewDate.getMonth();
144
+ const weekDays = ['일', '월', '화', '수', '목', '금', '토'];
145
+ // 년도 옵션: 현재 년도 ±10년
146
+ const currentYear = today.getFullYear();
147
+ const yearOptions = Array.from({ length: 21 }, (_, i) => currentYear - 10 + i);
148
+ // 월 옵션: 1~12월
149
+ const monthOptions = Array.from({ length: 12 }, (_, i) => i);
150
+ // minViewDate/maxViewDate 기반 제한 계산
151
+ const minYear = minViewDate?.getFullYear();
152
+ const minMonth = minViewDate?.getMonth();
153
+ const maxYear = maxViewDate?.getFullYear();
154
+ const maxMonth = maxViewDate?.getMonth();
155
+ // 이전 월 버튼 비활성화 여부
156
+ const isPrevDisabled = minViewDate
157
+ ? year < minYear || (year === minYear && month <= minMonth)
158
+ : false;
159
+ // 다음 월 버튼 비활성화 여부
160
+ const isNextDisabled = maxViewDate
161
+ ? year > maxYear || (year === maxYear && month >= maxMonth)
162
+ : false;
163
+ // 년도 옵션 필터링 (선택 가능한 년도만)
164
+ const filteredYearOptions = yearOptions.filter((y) => {
165
+ if (minYear !== undefined && y < minYear)
166
+ return false;
167
+ if (maxYear !== undefined && y > maxYear)
168
+ return false;
169
+ return true;
170
+ });
171
+ // 월 옵션 필터링 (현재 선택된 년도에서 선택 가능한 월만)
172
+ const filteredMonthOptions = monthOptions.filter((m) => {
173
+ if (minYear !== undefined && minMonth !== undefined) {
174
+ if (year === minYear && m < minMonth)
175
+ return false;
176
+ }
177
+ if (maxYear !== undefined && maxMonth !== undefined) {
178
+ if (year === maxYear && m > maxMonth)
179
+ return false;
180
+ }
181
+ return true;
182
+ });
183
+ const handlePrevMonth = () => {
184
+ if (isPrevDisabled)
185
+ return;
186
+ onViewDateChange(new Date(year, month - 1, 1));
187
+ };
188
+ const handleNextMonth = () => {
189
+ if (isNextDisabled)
190
+ return;
191
+ onViewDateChange(new Date(year, month + 1, 1));
192
+ };
193
+ const handleYearChange = (e) => {
194
+ onViewDateChange(new Date(parseInt(e.target.value), month, 1));
195
+ };
196
+ const handleMonthChange = (e) => {
197
+ onViewDateChange(new Date(year, parseInt(e.target.value), 1));
198
+ };
199
+ const renderDaysView = () => {
200
+ const daysInMonth = getDaysInMonth(year, month);
201
+ const firstDay = getFirstDayOfMonth(year, month);
202
+ const prevMonthDays = getDaysInMonth(year, month - 1);
203
+ const days = [];
204
+ // 날짜 비활성화 확인 (disable/enable + minDate/maxDate)
205
+ const checkDateDisabled = (date) => {
206
+ if (isDateDisabled(date, disable, enable))
207
+ return true;
208
+ if (isBeforeMinDate(date, minDate))
209
+ return true;
210
+ if (isAfterMaxDate(date, maxDate))
211
+ return true;
212
+ return false;
213
+ };
214
+ // Previous month days
215
+ for (let i = firstDay - 1; i >= 0; i--) {
216
+ const day = prevMonthDays - i;
217
+ const prevDate = new Date(year, month - 1, day);
218
+ const isDisabled = checkDateDisabled(prevDate);
219
+ days.push(_jsx("button", { type: "button", className: `${styles.calendarCell} ${styles.other} ${isDisabled ? styles.disabled : ''}`, onClick: () => !isDisabled && onSelect(prevDate), disabled: isDisabled, children: day }, `prev-${day}`));
220
+ }
221
+ // Current month days
222
+ for (let day = 1; day <= daysInMonth; day++) {
223
+ const date = new Date(year, month, day);
224
+ const isToday = isSameDay(date, today);
225
+ const isSelected = value && isSameDay(date, value);
226
+ const isRangeStart = mode === 'period' && value && isSameDay(date, value);
227
+ const isRangeEnd = mode === 'period' && endValue && isSameDay(date, endValue);
228
+ const isInRangeDay = mode === 'period' && value && endValue && isInRangeExclusive(date, value, endValue);
229
+ const isDisabled = checkDateDisabled(date);
230
+ let cellClass = styles.calendarCell;
231
+ if (isDisabled)
232
+ cellClass += ` ${styles.disabled}`;
233
+ if (isToday && !isSelected && !isRangeStart && !isRangeEnd)
234
+ cellClass += ` ${styles.today}`;
235
+ if (mode === 'instant' && isSelected)
236
+ cellClass += ` ${styles.selected}`;
237
+ if (isRangeStart)
238
+ cellClass += ` ${styles.rangeStart}`;
239
+ if (isRangeEnd)
240
+ cellClass += ` ${styles.rangeEnd}`;
241
+ if (isInRangeDay)
242
+ cellClass += ` ${styles.inRange}`;
243
+ days.push(_jsx("button", { type: "button", className: cellClass, onClick: () => !isDisabled && onSelect(date), disabled: isDisabled, children: day }, day));
244
+ }
245
+ // Next month days
246
+ const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7;
247
+ const remainingDays = totalCells - (firstDay + daysInMonth);
248
+ for (let day = 1; day <= remainingDays; day++) {
249
+ const nextDate = new Date(year, month + 1, day);
250
+ const isDisabled = checkDateDisabled(nextDate);
251
+ days.push(_jsx("button", { type: "button", className: `${styles.calendarCell} ${styles.other} ${isDisabled ? styles.disabled : ''}`, onClick: () => !isDisabled && onSelect(nextDate), disabled: isDisabled, children: day }, `next-${day}`));
252
+ }
253
+ // Group into rows
254
+ const rows = [];
255
+ for (let i = 0; i < days.length; i += 7) {
256
+ rows.push(_jsx("div", { className: styles.calendarRow, children: days.slice(i, i + 7) }, i));
257
+ }
258
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.calendarRow, children: weekDays.map((day) => (_jsx("div", { className: `${styles.calendarCell} ${styles.header}`, children: day }, day))) }), rows] }));
259
+ };
260
+ return (_jsxs("div", { className: styles.calendar, children: [_jsxs("div", { className: styles.calendarNav, children: [showPrevNav ? (_jsx("button", { type: "button", className: styles.navButton, onClick: handlePrevMonth, disabled: isPrevDisabled, children: _jsx("i", { className: "icon-expand-left" }) })) : (_jsx("div", { className: styles.navButtonPlaceholder })), _jsxs("div", { className: styles.navTitle, children: [_jsx("div", { className: styles.navSelectWrapper, children: _jsx("select", { className: styles.navSelect, value: year, onChange: handleYearChange, children: filteredYearOptions.map((y) => (_jsxs("option", { value: y, children: [y, "\uB144"] }, y))) }) }), _jsx("div", { className: styles.navSelectWrapper, children: _jsx("select", { className: styles.navSelect, value: month, onChange: handleMonthChange, children: filteredMonthOptions.map((m) => (_jsxs("option", { value: m, children: [m + 1, "\uC6D4"] }, m))) }) })] }), showNextNav ? (_jsx("button", { type: "button", className: styles.navButton, onClick: handleNextMonth, disabled: isNextDisabled, children: _jsx("i", { className: "icon-expand-right" }) })) : (_jsx("div", { className: styles.navButtonPlaceholder }))] }), _jsx("div", { className: styles.calendarGrid, children: renderDaysView() })] }));
261
+ };
262
+ const PeriodCalendar = ({ value, endValue, onSelect, viewDate, endViewDate, onViewDateChange, onEndViewDateChange, disable, enable, minDate, maxDate, }) => {
263
+ // 왼쪽 달력: 오른쪽 달력(endViewDate)보다 이후로 이동 불가
264
+ // 오른쪽 달력: 왼쪽 달력(viewDate)보다 이전으로 이동 불가
265
+ return (_jsxs("div", { className: styles.periodCalendars, children: [_jsx("div", { className: styles.periodCalendarLeft, children: _jsx(Calendar, { value: value, endValue: endValue, mode: "period", onSelect: onSelect, viewDate: viewDate, onViewDateChange: onViewDateChange, showPrevNav: true, showNextNav: true, maxViewDate: endViewDate, disable: disable, enable: enable, minDate: minDate, maxDate: maxDate }) }), _jsx("div", { className: styles.periodCalendarRight, children: _jsx(Calendar, { value: value, endValue: endValue, mode: "period", onSelect: onSelect, viewDate: endViewDate, onViewDateChange: onEndViewDateChange, showPrevNav: true, showNextNav: true, minViewDate: viewDate, disable: disable, enable: enable, minDate: minDate, maxDate: maxDate }) })] }));
266
+ };
267
+ // Main DatePicker Component
268
+ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placeholder, disabled = false, showActions, align = 'left', className, disable, enable, minDate, maxDate, minuteStep = 1, }) => {
269
+ const [selectingPart, setSelectingPart] = useState(null);
270
+ const [tempValue, setTempValue] = useState(value || {});
271
+ const [viewDate, setViewDate] = useState(value?.date || new Date());
272
+ const [endViewDate, setEndViewDate] = useState(() => {
273
+ const d = value?.endDate || new Date();
274
+ return new Date(d.getFullYear(), d.getMonth() + 1, 1);
275
+ });
276
+ const containerRef = useRef(null);
277
+ const shouldShowActions = showActions ?? mode === 'period';
278
+ // 날짜 선택 시에만 드롭다운 표시 (시/분은 native select 사용)
279
+ const isOpen = selectingPart === 'date' || selectingPart === 'endDate';
280
+ // Close on outside click
281
+ useEffect(() => {
282
+ const handleClickOutside = (event) => {
283
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
284
+ setSelectingPart(null);
285
+ }
286
+ };
287
+ document.addEventListener('mousedown', handleClickOutside);
288
+ return () => document.removeEventListener('mousedown', handleClickOutside);
289
+ }, []);
290
+ // Sync temp value with prop value
291
+ useEffect(() => {
292
+ setTempValue(value || {});
293
+ }, [value]);
294
+ const formatPeriodText = () => {
295
+ if (!tempValue.date)
296
+ return '';
297
+ const formatKoreanDateTime = (date, time) => {
298
+ const year = date.getFullYear();
299
+ const month = date.getMonth() + 1;
300
+ const day = date.getDate();
301
+ const dateStr = `${year}년 ${month}월 ${day}일`;
302
+ if (type === 'datetime' && time) {
303
+ const hours = String(time.hour).padStart(2, '0');
304
+ const minutes = String(time.minute).padStart(2, '0');
305
+ return `${dateStr} ${hours}:${minutes}`;
306
+ }
307
+ return dateStr;
308
+ };
309
+ const startText = formatKoreanDateTime(tempValue.date, tempValue.time);
310
+ if (tempValue.endDate) {
311
+ const endText = formatKoreanDateTime(tempValue.endDate, tempValue.endTime);
312
+ return `${startText} ~ ${endText}`;
313
+ }
314
+ return startText;
315
+ };
316
+ // 날짜 선택 시 시간을 minDate/maxDate 범위 내로 자동 보정
317
+ const adjustTimeForDate = (date, time) => {
318
+ if (!time)
319
+ return time;
320
+ const minLimit = minDate ? extractDateTimeLimit(minDate) : null;
321
+ const maxLimit = maxDate ? extractDateTimeLimit(maxDate) : null;
322
+ let adjustedHour = time.hour;
323
+ let adjustedMinute = time.minute;
324
+ // minDate와 같은 날짜인 경우
325
+ if (minLimit?.time && isSameDay(date, minLimit.date)) {
326
+ if (adjustedHour < minLimit.time.hour) {
327
+ adjustedHour = minLimit.time.hour;
328
+ // minuteStep에 맞춰 올림
329
+ adjustedMinute = Math.ceil(minLimit.time.minute / minuteStep) * minuteStep;
330
+ }
331
+ else if (adjustedHour === minLimit.time.hour && adjustedMinute < minLimit.time.minute) {
332
+ // minuteStep에 맞춰 올림
333
+ adjustedMinute = Math.ceil(minLimit.time.minute / minuteStep) * minuteStep;
334
+ }
335
+ }
336
+ // maxDate와 같은 날짜인 경우
337
+ if (maxLimit?.time && isSameDay(date, maxLimit.date)) {
338
+ if (adjustedHour > maxLimit.time.hour) {
339
+ adjustedHour = maxLimit.time.hour;
340
+ // minuteStep에 맞춰 내림
341
+ adjustedMinute = Math.floor(maxLimit.time.minute / minuteStep) * minuteStep;
342
+ }
343
+ else if (adjustedHour === maxLimit.time.hour && adjustedMinute > maxLimit.time.minute) {
344
+ // minuteStep에 맞춰 내림
345
+ adjustedMinute = Math.floor(maxLimit.time.minute / minuteStep) * minuteStep;
346
+ }
347
+ }
348
+ if (adjustedHour !== time.hour || adjustedMinute !== time.minute) {
349
+ return { hour: adjustedHour, minute: adjustedMinute };
350
+ }
351
+ return time;
352
+ };
353
+ const handleDateSelect = (date) => {
354
+ // 날짜만 저장 (시간은 별도 time/endTime으로 관리)
355
+ const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
356
+ // instant 모드: 단일 날짜 선택 후 닫기
357
+ if (mode === 'instant') {
358
+ const adjustedTime = adjustTimeForDate(newDate, tempValue.time);
359
+ const newValue = { ...tempValue, date: newDate, time: adjustedTime };
360
+ setTempValue(newValue);
361
+ if (!shouldShowActions) {
362
+ onChange?.(newValue);
363
+ }
364
+ setSelectingPart(null);
365
+ return;
366
+ }
367
+ // period 모드: 기간 선택 로직
368
+ // 시작일/종료일이 같을 수 있음
369
+ // 시작일이 종료일보다 이후일 수 없음 (자동 정렬)
370
+ // 종료일이 시작일보다 이전일 수 없음 (자동 정렬)
371
+ // 하나만 선택해도 드롭다운 유지 (적용 버튼으로 닫음)
372
+ const existingStartDate = tempValue.date;
373
+ const existingEndDate = tempValue.endDate;
374
+ if (!existingStartDate) {
375
+ // 첫 번째 날짜 선택: 시작일 설정
376
+ const adjustedTime = adjustTimeForDate(newDate, tempValue.time);
377
+ setTempValue({ ...tempValue, date: newDate, time: adjustedTime });
378
+ // 드롭다운 유지 - 종료일 선택 대기
379
+ return;
380
+ }
381
+ if (!existingEndDate) {
382
+ // 두 번째 날짜 선택: 시작일/종료일 자동 정렬
383
+ const dateOnly = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
384
+ const startDateOnly = new Date(existingStartDate.getFullYear(), existingStartDate.getMonth(), existingStartDate.getDate());
385
+ if (dateOnly < startDateOnly) {
386
+ // 선택한 날짜가 시작일보다 이전 → 스왑 (선택한 날짜가 시작일, 기존 시작일이 종료일)
387
+ // 시간도 함께 스왑하고 범위 보정
388
+ const adjustedStartTime = adjustTimeForDate(newDate, tempValue.time);
389
+ const adjustedEndTime = adjustTimeForDate(existingStartDate, tempValue.time);
390
+ setTempValue({
391
+ date: newDate,
392
+ time: adjustedStartTime,
393
+ endDate: existingStartDate,
394
+ endTime: adjustedEndTime,
395
+ });
396
+ }
397
+ else {
398
+ // 선택한 날짜가 시작일 이후/같음 → 종료일로 설정
399
+ const adjustedEndTime = adjustTimeForDate(newDate, tempValue.endTime);
400
+ setTempValue({ ...tempValue, endDate: newDate, endTime: adjustedEndTime });
401
+ }
402
+ // 드롭다운 유지 - 적용 버튼으로 닫음
403
+ return;
404
+ }
405
+ // 이미 둘 다 선택된 경우 → 새로운 시작일로 리셋
406
+ const adjustedTime = adjustTimeForDate(newDate, tempValue.time);
407
+ setTempValue({ date: newDate, time: adjustedTime, endDate: undefined, endTime: undefined });
408
+ };
409
+ const handleReset = () => {
410
+ setTempValue({});
411
+ setSelectingPart(null);
412
+ };
413
+ const handleApply = () => {
414
+ onChange?.(tempValue);
415
+ setSelectingPart(null);
416
+ };
417
+ const handlePartClick = (part) => {
418
+ if (disabled)
419
+ return;
420
+ setSelectingPart(selectingPart === part ? null : part);
421
+ };
422
+ const displayValue = shouldShowActions ? tempValue : value;
423
+ const hasStartValue = !!displayValue?.date;
424
+ const hasEndValue = !!displayValue?.endDate;
425
+ // Helper to render date button
426
+ const renderDateButton = (date, isActive, onClick, isPlaceholder) => {
427
+ if (isPlaceholder || !date) {
428
+ return (_jsx("button", { type: "button", className: `${styles.inputPart} ${isActive ? styles.active : ''} ${styles.placeholder}`, onClick: onClick, children: "YYYY - MM - DD" }));
429
+ }
430
+ const year = date.getFullYear();
431
+ const month = String(date.getMonth() + 1).padStart(2, '0');
432
+ const day = String(date.getDate()).padStart(2, '0');
433
+ return (_jsxs("button", { type: "button", className: `${styles.inputPart} ${isActive ? styles.active : ''}`, onClick: onClick, children: [year, " - ", month, " - ", day] }));
434
+ };
435
+ // Helper to render hour select
436
+ const renderHourSelect = (time, part, isPlaceholder) => {
437
+ const hour = time?.hour ?? 0;
438
+ const hours = Array.from({ length: 24 }, (_, i) => i);
439
+ const isEnd = part === 'endHour';
440
+ const currentDate = isEnd ? tempValue.endDate : tempValue.date;
441
+ // minDate/maxDate 시간 제한 계산
442
+ const minLimit = minDate ? extractDateTimeLimit(minDate) : null;
443
+ const maxLimit = maxDate ? extractDateTimeLimit(maxDate) : null;
444
+ const isHourDisabled = (h) => {
445
+ if (!currentDate)
446
+ return false;
447
+ // minDate와 같은 날짜인 경우
448
+ if (minLimit?.time && isSameDay(currentDate, minLimit.date)) {
449
+ if (h < minLimit.time.hour)
450
+ return true;
451
+ }
452
+ // maxDate와 같은 날짜인 경우
453
+ if (maxLimit?.time && isSameDay(currentDate, maxLimit.date)) {
454
+ if (h > maxLimit.time.hour)
455
+ return true;
456
+ }
457
+ return false;
458
+ };
459
+ const handleChange = (e) => {
460
+ const selectedHour = parseInt(e.target.value, 10);
461
+ const currentTime = isEnd ? tempValue.endTime : tempValue.time;
462
+ let newMinute = currentTime?.minute ?? 0;
463
+ // 시간 변경 시 분이 범위를 벗어나면 자동 보정
464
+ if (currentDate) {
465
+ // minDate와 같은 날짜이고 선택한 시간이 minLimit 시간과 같은 경우
466
+ if (minLimit?.time && isSameDay(currentDate, minLimit.date) && selectedHour === minLimit.time.hour) {
467
+ if (newMinute < minLimit.time.minute) {
468
+ // minuteStep에 맞춰 올림
469
+ newMinute = Math.ceil(minLimit.time.minute / minuteStep) * minuteStep;
470
+ }
471
+ }
472
+ // maxDate와 같은 날짜이고 선택한 시간이 maxLimit 시간과 같은 경우
473
+ if (maxLimit?.time && isSameDay(currentDate, maxLimit.date) && selectedHour === maxLimit.time.hour) {
474
+ if (newMinute > maxLimit.time.minute) {
475
+ // minuteStep에 맞춰 내림
476
+ newMinute = Math.floor(maxLimit.time.minute / minuteStep) * minuteStep;
477
+ }
478
+ }
479
+ }
480
+ // minuteStep에 맞지 않는 분 값 보정
481
+ if (newMinute % minuteStep !== 0) {
482
+ newMinute = Math.floor(newMinute / minuteStep) * minuteStep;
483
+ }
484
+ const newTime = { hour: selectedHour, minute: newMinute };
485
+ const newValue = isEnd
486
+ ? { ...tempValue, endTime: newTime }
487
+ : { ...tempValue, time: newTime };
488
+ setTempValue(newValue);
489
+ if (!shouldShowActions) {
490
+ onChange?.(newValue);
491
+ }
492
+ };
493
+ return (_jsx("select", { className: `${styles.timeSelect} ${isPlaceholder ? styles.placeholder : ''}`, value: hour, onChange: handleChange, disabled: disabled, children: hours.map((h) => (_jsx("option", { value: h, disabled: isHourDisabled(h), children: String(h).padStart(2, '0') }, h))) }));
494
+ };
495
+ // Helper to render minute select
496
+ const renderMinuteSelect = (time, part, isPlaceholder) => {
497
+ const minute = time?.minute ?? 0;
498
+ // minuteStep에 따라 분 옵션 생성 (0, step, step*2, ...)
499
+ const minutes = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep);
500
+ const isEnd = part === 'endMinute';
501
+ const currentDate = isEnd ? tempValue.endDate : tempValue.date;
502
+ const currentTime = isEnd ? tempValue.endTime : tempValue.time;
503
+ // minDate/maxDate 시간 제한 계산
504
+ const minLimit = minDate ? extractDateTimeLimit(minDate) : null;
505
+ const maxLimit = maxDate ? extractDateTimeLimit(maxDate) : null;
506
+ const isMinuteDisabled = (m) => {
507
+ if (!currentDate || !currentTime)
508
+ return false;
509
+ // minDate와 같은 날짜이고 같은 시간인 경우
510
+ if (minLimit?.time && isSameDay(currentDate, minLimit.date) && currentTime.hour === minLimit.time.hour) {
511
+ if (m < minLimit.time.minute)
512
+ return true;
513
+ }
514
+ // maxDate와 같은 날짜이고 같은 시간인 경우
515
+ if (maxLimit?.time && isSameDay(currentDate, maxLimit.date) && currentTime.hour === maxLimit.time.hour) {
516
+ if (m > maxLimit.time.minute)
517
+ return true;
518
+ }
519
+ return false;
520
+ };
521
+ const handleChange = (e) => {
522
+ let selectedMinute = parseInt(e.target.value, 10);
523
+ const selectedHour = currentTime?.hour ?? 0;
524
+ // 분 선택 시 범위 자동 보정
525
+ if (currentDate) {
526
+ // minDate와 같은 날짜이고 같은 시간인 경우
527
+ if (minLimit?.time && isSameDay(currentDate, minLimit.date) && selectedHour === minLimit.time.hour) {
528
+ if (selectedMinute < minLimit.time.minute) {
529
+ // minuteStep에 맞춰 올림
530
+ selectedMinute = Math.ceil(minLimit.time.minute / minuteStep) * minuteStep;
531
+ }
532
+ }
533
+ // maxDate와 같은 날짜이고 같은 시간인 경우
534
+ if (maxLimit?.time && isSameDay(currentDate, maxLimit.date) && selectedHour === maxLimit.time.hour) {
535
+ if (selectedMinute > maxLimit.time.minute) {
536
+ // minuteStep에 맞춰 내림
537
+ selectedMinute = Math.floor(maxLimit.time.minute / minuteStep) * minuteStep;
538
+ }
539
+ }
540
+ }
541
+ const newTime = { hour: selectedHour, minute: selectedMinute };
542
+ const newValue = isEnd
543
+ ? { ...tempValue, endTime: newTime }
544
+ : { ...tempValue, time: newTime };
545
+ setTempValue(newValue);
546
+ if (!shouldShowActions) {
547
+ onChange?.(newValue);
548
+ }
549
+ };
550
+ // 현재 값이 minuteStep에 맞지 않으면 가장 가까운 값으로 표시
551
+ const displayMinute = minutes.includes(minute) ? minute : Math.floor(minute / minuteStep) * minuteStep;
552
+ return (_jsx("select", { className: `${styles.timeSelect} ${isPlaceholder ? styles.placeholder : ''}`, value: displayMinute, onChange: handleChange, disabled: disabled, children: minutes.map((m) => (_jsx("option", { value: m, disabled: isMinuteDisabled(m), children: String(m).padStart(2, '0') }, m))) }));
553
+ };
554
+ // Render input content with clickable parts
555
+ const renderInputContent = () => {
556
+ const hasStartTime = displayValue?.time !== undefined;
557
+ const hasEndTime = displayValue?.endTime !== undefined;
558
+ if (type === 'date') {
559
+ if (mode === 'period') {
560
+ return (_jsxs("div", { className: styles.inputContent, children: [renderDateButton(displayValue?.date, selectingPart === 'date', () => handlePartClick('date'), !hasStartValue), _jsx("span", { className: styles.separator, children: "~" }), renderDateButton(displayValue?.endDate, selectingPart === 'endDate', () => handlePartClick('endDate'), !hasEndValue)] }));
561
+ }
562
+ return (_jsx("div", { className: styles.inputContent, children: renderDateButton(displayValue?.date, selectingPart === 'date', () => handlePartClick('date'), !hasStartValue) }));
563
+ }
564
+ if (type === 'time') {
565
+ if (mode === 'period') {
566
+ return (_jsxs("div", { className: styles.inputContent, children: [_jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.time, 'hour', !hasStartTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.time, 'minute', !hasStartTime)] }), _jsx("span", { className: styles.separator, children: "~" }), _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.endTime, 'endHour', !hasEndTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.endTime, 'endMinute', !hasEndTime)] })] }));
567
+ }
568
+ return (_jsx("div", { className: styles.inputContent, children: _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.time, 'hour', !hasStartTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.time, 'minute', !hasStartTime)] }) }));
569
+ }
570
+ // datetime
571
+ if (mode === 'period') {
572
+ return (_jsxs("div", { className: styles.inputContent, children: [renderDateButton(displayValue?.date, selectingPart === 'date', () => handlePartClick('date'), !hasStartValue), _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.time, 'hour', !hasStartTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.time, 'minute', !hasStartTime)] }), _jsx("span", { className: styles.separator, children: "~" }), renderDateButton(displayValue?.endDate, selectingPart === 'endDate', () => handlePartClick('endDate'), !hasEndValue), _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.endTime, 'endHour', !hasEndTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.endTime, 'endMinute', !hasEndTime)] })] }));
573
+ }
574
+ return (_jsxs("div", { className: styles.inputContent, children: [renderDateButton(displayValue?.date, selectingPart === 'date', () => handlePartClick('date'), !hasStartValue), _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.time, 'hour', !hasStartTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.time, 'minute', !hasStartTime)] })] }));
575
+ };
576
+ // Render dropdown content based on selecting part
577
+ const renderDropdownContent = () => {
578
+ // 날짜 선택만 드롭다운으로 표시 (시/분은 native select 사용)
579
+ if (selectingPart === 'date' || selectingPart === 'endDate') {
580
+ // period 모드: 두 개의 달력을 나란히 표시
581
+ if (mode === 'period') {
582
+ return (_jsx(PeriodCalendar, { value: tempValue.date, endValue: tempValue.endDate, onSelect: handleDateSelect, viewDate: viewDate, endViewDate: endViewDate, onViewDateChange: setViewDate, onEndViewDateChange: setEndViewDate, disable: disable, enable: enable, minDate: minDate, maxDate: maxDate }));
583
+ }
584
+ // instant 모드: 단일 달력
585
+ return (_jsx(Calendar, { value: tempValue.date, endValue: tempValue.endDate, mode: mode, onSelect: handleDateSelect, viewDate: viewDate, onViewDateChange: setViewDate, disable: disable, enable: enable, minDate: minDate, maxDate: maxDate }));
586
+ }
587
+ return null;
588
+ };
589
+ // 아이콘 결정 (time 타입은 icon-time, 나머지는 icon-calendar)
590
+ const inputIcon = type === 'time' ? 'icon-time' : 'icon-calendar';
591
+ return (_jsxs("div", { ref: containerRef, className: `${styles.datepicker} ${className || ''}`, children: [_jsxs("div", { className: `${styles.input} ${isOpen ? styles.active : ''} ${disabled ? styles.disabled : ''}`, children: [renderInputContent(), _jsx("i", { className: `${styles.inputIcon} ${inputIcon}` })] }), isOpen && (_jsxs("div", { className: `${styles.dropdown} ${align === 'right' ? styles.right : ''}`, children: [renderDropdownContent(), shouldShowActions && (_jsxs("div", { className: styles.bottomActions, children: [_jsx("span", { className: styles.periodText, children: mode === 'period' && tempValue.date ? formatPeriodText() : '' }), _jsxs("div", { className: styles.actionButtons, children: [_jsxs("button", { type: "button", className: `${styles.actionButton} ${styles.reset}`, onClick: handleReset, children: [_jsx("i", { className: "icon-refresh" }), "\uCD08\uAE30\uD654"] }), _jsx("button", { type: "button", className: `${styles.actionButton} ${styles.apply}`, onClick: handleApply, children: "\uC801\uC6A9" })] })] }))] }))] }));
592
+ };
593
+ export default DatePicker;
@@ -0,0 +1,635 @@
1
+ @use '../../../mixin.scss' as *;
2
+
3
+ // DatePicker Input
4
+ .datepicker {
5
+ position: relative;
6
+ display: inline-block;
7
+ }
8
+
9
+ .input {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: s(2);
13
+ padding: s(3) s(4);
14
+ background: color('bg-block');
15
+ border: 1px solid color('border');
16
+ border-radius: r(3);
17
+ cursor: pointer;
18
+ transition: border-color 0.2s;
19
+
20
+ &:hover {
21
+ border-color: color('border-hover');
22
+ }
23
+
24
+ &:focus-within,
25
+ &.active {
26
+ border-color: color('primary');
27
+ }
28
+
29
+ &.disabled {
30
+ background: color('bg-disabled');
31
+ cursor: not-allowed;
32
+ opacity: 0.6;
33
+ }
34
+ }
35
+
36
+ // Input Content (피그마 디자인에 맞춘 구조)
37
+ .inputContent {
38
+ flex: 1;
39
+ display: flex;
40
+ align-items: center;
41
+ gap: s(2);
42
+ }
43
+
44
+ // 개별 클릭 가능한 입력 파트 (날짜용)
45
+ .inputPart {
46
+ @include p4;
47
+ display: flex;
48
+ flex: 1;
49
+ align-items: center;
50
+ justify-content: center;
51
+ padding: 0 s(2);
52
+ background: none;
53
+ border: none;
54
+ border-radius: r(4);
55
+ cursor: pointer;
56
+ transition: background 0.2s;
57
+ color: color('text-body');
58
+ white-space: nowrap;
59
+ font-variant-numeric: tabular-nums;
60
+
61
+ &:hover {
62
+ background: color('default');
63
+ }
64
+
65
+ &.active {
66
+ background: color('default-fill');
67
+ }
68
+
69
+ &.placeholder {
70
+ color: color('text-action-disabled');
71
+ }
72
+ }
73
+
74
+ .timeSection {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ gap: 0;
79
+ }
80
+
81
+ .timeSelect {
82
+ @include p4;
83
+ appearance: none;
84
+ -webkit-appearance: none;
85
+ -moz-appearance: none;
86
+ background: none;
87
+ border: none;
88
+ padding: 0 s(2);
89
+ flex: 1;
90
+ text-align: center;
91
+ text-align-last: center;
92
+ color: color('text-body');
93
+ cursor: pointer;
94
+ border-radius: r(4);
95
+ transition: background 0.2s;
96
+ font-variant-numeric: tabular-nums;
97
+
98
+ &:hover {
99
+ background: color('default');
100
+ }
101
+
102
+ &:focus {
103
+ outline: none;
104
+ background: color('default-fill');
105
+ }
106
+
107
+ &.placeholder {
108
+ color: color('text-action-disabled');
109
+ }
110
+
111
+ &:disabled {
112
+ cursor: not-allowed;
113
+ opacity: 0.6;
114
+ }
115
+ }
116
+
117
+ .timeSeparator {
118
+ @include p4;
119
+ color: color('text-body');
120
+ }
121
+
122
+ .separator {
123
+ @include p4;
124
+ color: color('text-body');
125
+ padding: 0 s(1);
126
+ }
127
+
128
+ .inputIcon {
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ width: 20px;
133
+ height: 20px;
134
+ font-size: 20px;
135
+ line-height: 1;
136
+ color: color('text-sub');
137
+ flex-shrink: 0;
138
+ }
139
+
140
+ // Dropdown
141
+ .dropdown {
142
+ position: absolute;
143
+ top: calc(100% + s(2));
144
+ left: 0;
145
+ z-index: 1000;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 10px;
149
+ padding: s(5);
150
+ background: color('bg-modal');
151
+ border-radius: s(3);
152
+ box-shadow: 0 6px 18px -3px rgba(50, 50, 50, 0.06);
153
+
154
+ &.right {
155
+ left: auto;
156
+ right: 0;
157
+ }
158
+ }
159
+
160
+ // Calendar
161
+ .calendar {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 10px;
165
+ min-width: 280px;
166
+ }
167
+
168
+ .calendarNav {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: s(4);
172
+ height: 40px;
173
+ }
174
+
175
+ .navButton {
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ width: 40px;
180
+ height: 40px;
181
+ background: color('default');
182
+ border: none;
183
+ border-radius: r(3);
184
+ cursor: pointer;
185
+ transition: background 0.2s;
186
+
187
+ &:hover {
188
+ background: color('default-hover');
189
+ }
190
+
191
+ &:disabled {
192
+ opacity: 0.5;
193
+ cursor: not-allowed;
194
+ }
195
+
196
+ i {
197
+ font-size: 20px;
198
+ color: color('text-action');
199
+ }
200
+ }
201
+
202
+ .navButtonPlaceholder {
203
+ width: 40px;
204
+ height: 40px;
205
+ }
206
+
207
+ .navTitle {
208
+ flex: 1;
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ gap: s(1);
213
+ }
214
+
215
+ .navTitleButton {
216
+ @include display7;
217
+ display: flex;
218
+ align-items: center;
219
+ gap: s(2);
220
+ padding: 0 s(2);
221
+ background: none;
222
+ border: none;
223
+ border-radius: r(4);
224
+ cursor: pointer;
225
+ transition: background 0.2s;
226
+
227
+ &:hover {
228
+ background: color('default');
229
+ }
230
+
231
+ i {
232
+ font-size: 18px;
233
+ color: color('text-action');
234
+ }
235
+ }
236
+
237
+ .navSelectWrapper {
238
+ position: relative;
239
+ display: flex;
240
+ align-items: center;
241
+
242
+ &::after {
243
+ content: '';
244
+ position: absolute;
245
+ right: s(2);
246
+ top: 50%;
247
+ transform: translateY(-50%);
248
+ width: 0;
249
+ height: 0;
250
+ border-left: 4px solid transparent;
251
+ border-right: 4px solid transparent;
252
+ border-top: 5px solid color('text-action');
253
+ pointer-events: none;
254
+ }
255
+ }
256
+
257
+ .navSelect {
258
+ @include display7;
259
+ appearance: none;
260
+ -webkit-appearance: none;
261
+ -moz-appearance: none;
262
+ background: none;
263
+ border: none;
264
+ padding: s(2) s(6) s(2) s(2);
265
+ border-radius: r(4);
266
+ cursor: pointer;
267
+ transition: background 0.2s;
268
+ color: color('text-body');
269
+
270
+ &:hover {
271
+ background: color('default');
272
+ }
273
+
274
+ &:focus {
275
+ outline: none;
276
+ background: color('default-fill');
277
+ }
278
+ }
279
+
280
+ .calendarGrid {
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: s(2);
284
+ padding-top: s(3);
285
+ border-top: 1px solid color('border');
286
+ }
287
+
288
+ .calendarRow {
289
+ display: flex;
290
+ width: 100%;
291
+ }
292
+
293
+ .calendarCell {
294
+ flex: 1;
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ height: 40px;
299
+ @include p3;
300
+ background: none;
301
+ border: none;
302
+ border-radius: r(3);
303
+ cursor: pointer;
304
+ transition: background 0.2s;
305
+
306
+ &.header {
307
+ color: color('text-sub');
308
+ cursor: default;
309
+ }
310
+
311
+ &.other {
312
+ color: color('text-action-disabled');
313
+ }
314
+
315
+ &.today {
316
+ font-weight: 600;
317
+ color: color('primary');
318
+ }
319
+
320
+ &.selected {
321
+ background: color('primary');
322
+ color: color('primary-reverse');
323
+ font-weight: 600;
324
+ }
325
+
326
+ &.inRange {
327
+ background: color('primary-fill');
328
+ border-radius: 0;
329
+ }
330
+
331
+ &.rangeStart {
332
+ background: color('primary');
333
+ color: color('primary-reverse');
334
+ font-weight: 600;
335
+ border-radius: r(3);
336
+ }
337
+
338
+ &.rangeEnd {
339
+ background: color('primary');
340
+ color: color('primary-reverse');
341
+ font-weight: 600;
342
+ border-radius: r(3);
343
+ }
344
+
345
+ &.disabled {
346
+ color: color('text-action-disabled');
347
+ background: color('default');
348
+ border: none;
349
+ border-radius: 0;
350
+ outline: none;
351
+ cursor: not-allowed;
352
+
353
+ &:hover {
354
+ background: color('default');
355
+ }
356
+ }
357
+
358
+ &:not(.header):not(.selected):not(.disabled):not(.rangeStart):not(.rangeEnd):not(.inRange):hover {
359
+ background: color('default');
360
+ }
361
+ }
362
+
363
+ // Year/Month Selector
364
+ .yearMonthGrid {
365
+ display: grid;
366
+ grid-template-columns: repeat(3, 1fr);
367
+ gap: s(2);
368
+ padding-top: s(3);
369
+ border-top: 1px solid color('border');
370
+ }
371
+
372
+ .yearMonthCell {
373
+ @include p3;
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ height: 40px;
378
+ background: none;
379
+ border: none;
380
+ border-radius: r(3);
381
+ cursor: pointer;
382
+ transition: background 0.2s;
383
+
384
+ &:hover {
385
+ background: color('default');
386
+ }
387
+
388
+ &.selected {
389
+ background: color('primary');
390
+ color: color('primary-reverse');
391
+ font-weight: 600;
392
+ }
393
+
394
+ &.current {
395
+ font-weight: 600;
396
+ color: color('primary');
397
+ }
398
+ }
399
+
400
+ // Time Selector
401
+ .timeSelector {
402
+ display: flex;
403
+ flex-direction: column;
404
+ min-width: 120px;
405
+ background: color('bg-block');
406
+ border-radius: r(2);
407
+ }
408
+
409
+ .timeDisplay {
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ gap: s(1);
414
+ padding: s(2);
415
+ }
416
+
417
+ .timeDisplayItem {
418
+ @include display7;
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: center;
422
+ padding: s(2) s(3);
423
+ background: color('default');
424
+ border: none;
425
+ border-radius: r(2);
426
+ cursor: pointer;
427
+ transition: background 0.2s;
428
+ min-width: 48px;
429
+
430
+ &:hover {
431
+ background: color('default-hover');
432
+ }
433
+
434
+ &.active {
435
+ background: color('primary');
436
+ color: color('primary-reverse');
437
+ }
438
+ }
439
+
440
+ .timeDisplaySeparator {
441
+ @include display7;
442
+ color: color('text-body');
443
+ }
444
+
445
+ .timeColumn {
446
+ display: flex;
447
+ flex-direction: column;
448
+ gap: s(1);
449
+ padding: s(3);
450
+ max-height: 200px;
451
+ overflow-y: auto;
452
+ border-top: 1px solid color('border');
453
+
454
+ &::-webkit-scrollbar {
455
+ width: 4px;
456
+ }
457
+
458
+ &::-webkit-scrollbar-thumb {
459
+ background: color('border');
460
+ border-radius: r(full);
461
+ }
462
+ }
463
+
464
+ .timeItem {
465
+ @include p4;
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ padding: s(2) s(3);
470
+ background: none;
471
+ border: none;
472
+ border-radius: r(2);
473
+ cursor: pointer;
474
+ transition: background 0.2s;
475
+ text-align: center;
476
+
477
+ &:hover {
478
+ background: color('default');
479
+ }
480
+
481
+ &.selected {
482
+ background: color('primary');
483
+ color: color('primary-reverse');
484
+ font-weight: 600;
485
+ }
486
+ }
487
+
488
+ // Bottom Actions
489
+ .bottomActions {
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: space-between;
493
+ gap: s(3);
494
+ padding-top: s(3);
495
+ border-top: 1px solid color('border');
496
+ }
497
+
498
+ .periodText {
499
+ @include p3;
500
+ color: color('primary');
501
+ flex: 1;
502
+ }
503
+
504
+ .actionButtons {
505
+ display: flex;
506
+ gap: s(3);
507
+ flex-shrink: 0;
508
+ }
509
+
510
+ .actionButton {
511
+ @include p3;
512
+ display: inline-flex;
513
+ align-items: center;
514
+ justify-content: center;
515
+ gap: s(2);
516
+ padding: s(3) s(4);
517
+ border-radius: r(3);
518
+ cursor: pointer;
519
+ transition: background 0.2s;
520
+ white-space: nowrap;
521
+
522
+ i {
523
+ display: flex;
524
+ align-items: center;
525
+ justify-content: center;
526
+ width: 24px;
527
+ height: 24px;
528
+ font-size: 18px;
529
+ line-height: 1;
530
+ }
531
+
532
+ &.reset {
533
+ background: color('default-fill');
534
+ border: 1px solid color('border');
535
+ color: color('default-reverse');
536
+
537
+ &:hover {
538
+ background: color('default-hover');
539
+ }
540
+ }
541
+
542
+ &.apply {
543
+ background: color('primary');
544
+ border: none;
545
+ color: color('primary-reverse');
546
+
547
+ &:hover {
548
+ background: color('primary-hover');
549
+ }
550
+ }
551
+ }
552
+
553
+ // Combined Layout (datetime)
554
+ .combinedDropdown {
555
+ display: flex;
556
+ gap: s(5);
557
+ }
558
+
559
+ .calendarWrapper {
560
+ display: flex;
561
+ flex-direction: column;
562
+ }
563
+
564
+ .timeWrapper {
565
+ display: flex;
566
+ flex-direction: column;
567
+ border-left: 1px solid color('border');
568
+ padding-left: s(5);
569
+ }
570
+
571
+ .timeLabel {
572
+ @include p4;
573
+ color: color('text-sub');
574
+ margin-bottom: s(2);
575
+ text-align: center;
576
+ }
577
+
578
+ // Period Calendar Layout (두 개 캘린더가 하나의 드롭다운에서 가로로)
579
+ .periodCalendarWrapper {
580
+ display: flex;
581
+ flex-direction: column;
582
+ gap: s(4);
583
+ }
584
+
585
+ .periodCalendarNav {
586
+ display: flex;
587
+ align-items: center;
588
+ gap: s(4);
589
+ height: 40px;
590
+ }
591
+
592
+ .periodCalendarNavSection {
593
+ flex: 1;
594
+ display: flex;
595
+ align-items: center;
596
+ justify-content: center;
597
+ gap: s(4);
598
+ }
599
+
600
+ .periodCalendarBody {
601
+ display: flex;
602
+ gap: s(4);
603
+ padding-top: s(3);
604
+ border-top: 1px solid color('border');
605
+ }
606
+
607
+ .periodCalendarColumn {
608
+ flex: 1;
609
+ display: flex;
610
+ flex-direction: column;
611
+ gap: s(2);
612
+ }
613
+
614
+ // Period Calendars (두 개 달력 나란히)
615
+ .periodCalendars {
616
+ display: flex;
617
+ gap: s(4);
618
+ }
619
+
620
+ .periodCalendarLeft {
621
+ flex: 1;
622
+ }
623
+
624
+ .periodCalendarRight {
625
+ flex: 1;
626
+ }
627
+
628
+ .periodCalendar {
629
+ flex: 1;
630
+
631
+ &:not(:last-child) {
632
+ border-right: 1px solid color('border');
633
+ padding-right: s(4);
634
+ }
635
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "podo-ui",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "author": "hada0127 <work@tarucy.net>",
6
6
  "license": "MIT",
@@ -58,7 +58,7 @@
58
58
  "./package.json": "./package.json"
59
59
  },
60
60
  "scripts": {
61
- "dev": "next dev",
61
+ "dev": "next dev -p 5432",
62
62
  "build": "next build",
63
63
  "build:lib": "node ./cli/build-lib.js",
64
64
  "prepublishOnly": "npm run build:lib",