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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/react/molecule/datepicker.d.ts +64 -0
- package/dist/react/molecule/datepicker.d.ts.map +1 -0
- package/dist/react/molecule/datepicker.js +593 -0
- package/dist/react/molecule/datepicker.module.scss +635 -0
- package/package.json +2 -2
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|