@uniai-fe/uds-primitives 0.2.1 → 0.2.3
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/README.md +1 -1
- package/dist/styles.css +391 -81
- package/package.json +17 -8
- package/src/components/button/index.tsx +0 -2
- package/src/components/button/markup/Base.tsx +22 -1
- package/src/components/button/styles/button.scss +24 -2
- package/src/components/button/styles/variables.scss +4 -0
- package/src/components/button/types/index.ts +7 -0
- package/src/components/{input/img/calendar → calendar/img}/calendar.svg +5 -0
- package/src/components/calendar/index.tsx +5 -3
- package/src/components/calendar/markup/Core.tsx +67 -0
- package/src/components/calendar/markup/Icon.tsx +20 -0
- package/src/components/calendar/markup/Root.tsx +126 -0
- package/src/components/calendar/markup/index.tsx +24 -2
- package/src/components/calendar/markup/layout/Body.tsx +12 -0
- package/src/components/calendar/markup/layout/Container.tsx +43 -0
- package/src/components/calendar/markup/layout/Footer.tsx +12 -0
- package/src/components/calendar/markup/layout/Header.tsx +12 -0
- package/src/components/calendar/styles/index.scss +2 -0
- package/src/components/calendar/styles/layout.scss +21 -0
- package/src/components/calendar/styles/mantine-calendar.scss +240 -0
- package/src/components/calendar/types/calendar.ts +208 -0
- package/src/components/calendar/types/index.ts +1 -4
- package/src/components/calendar/utils/index.ts +1 -4
- package/src/components/calendar/utils/value-mapper.ts +24 -0
- package/src/components/checkbox/markup/Checkbox.tsx +31 -25
- package/src/components/dropdown/markup/index.tsx +10 -1
- package/src/components/input/hooks/index.ts +1 -0
- package/src/components/input/hooks/useAddress.ts +247 -0
- package/src/components/input/index.scss +5 -1
- package/src/components/input/markup/address/Button.tsx +65 -0
- package/src/components/input/markup/address/Template.tsx +135 -0
- package/src/components/input/markup/address/index.ts +9 -0
- package/src/components/input/markup/date/Template.tsx +181 -0
- package/src/components/input/markup/date/Trigger.tsx +79 -0
- package/src/components/input/markup/date/button/ApplyButton.tsx +38 -0
- package/src/components/input/markup/date/button/ClearButton.tsx +36 -0
- package/src/components/input/markup/date/button/TodayButton.tsx +36 -0
- package/src/components/input/markup/date/footer/Container.tsx +24 -0
- package/src/components/input/markup/date/footer/Template.tsx +36 -0
- package/src/components/input/markup/date/footer/UtilContainer.tsx +23 -0
- package/src/components/input/markup/date/footer/index.ts +3 -0
- package/src/components/input/markup/date/index.tsx +27 -0
- package/src/components/input/markup/foundation/Input.tsx +20 -1
- package/src/components/input/markup/index.tsx +4 -4
- package/src/components/input/styles/address.scss +24 -0
- package/src/components/input/styles/date.scss +45 -0
- package/src/components/input/styles/foundation.scss +28 -2
- package/src/components/input/styles/variables.scss +4 -0
- package/src/components/input/types/address.ts +249 -0
- package/src/components/input/types/date.ts +366 -0
- package/src/components/input/types/foundation.ts +6 -0
- package/src/components/input/types/index.ts +2 -1
- package/src/components/input/utils/address.ts +165 -0
- package/src/components/input/utils/date.ts +61 -0
- package/src/components/input/utils/index.tsx +2 -0
- package/src/components/pop-over/index.scss +1 -0
- package/src/components/pop-over/index.tsx +4 -0
- package/src/components/pop-over/markup/Content.tsx +77 -0
- package/src/components/pop-over/markup/Root.tsx +28 -0
- package/src/components/pop-over/markup/Trigger.tsx +26 -0
- package/src/components/pop-over/markup/index.tsx +17 -0
- package/src/components/pop-over/styles/base.scss +5 -0
- package/src/components/pop-over/styles/content.scss +24 -0
- package/src/components/pop-over/styles/index.scss +2 -0
- package/src/components/pop-over/types/index.ts +1 -0
- package/src/components/pop-over/types/pop-over.ts +86 -0
- package/src/components/radio/markup/Radio.tsx +10 -2
- package/src/components/radio/markup/RadioCard.tsx +6 -1
- package/src/components/radio/markup/RadioCardGroup.tsx +6 -1
- package/src/components/select/markup/Default.tsx +2 -0
- package/src/components/select/markup/foundation/Container.tsx +23 -0
- package/src/components/select/markup/multiple/Multiple.tsx +2 -0
- package/src/components/select/styles/select.scss +25 -2
- package/src/components/select/styles/variables.scss +4 -0
- package/src/components/select/types/props.ts +24 -5
- package/src/index.scss +1 -0
- package/src/index.tsx +3 -1
- package/src/init/mantine.css +5 -0
- package/src/init/mantine.ts +2 -0
- package/src/components/input/markup/calendar/Base.tsx +0 -329
- package/src/components/input/markup/calendar/index.tsx +0 -8
- package/src/components/input/styles/calendar.scss +0 -110
- package/src/components/input/styles/index.scss +0 -4
- package/src/components/input/types/calendar.ts +0 -208
- /package/src/components/{input/img/calendar → calendar/img}/chevron-down.svg +0 -0
- /package/src/components/{input/img/calendar → calendar/img}/chevron-left.svg +0 -0
- /package/src/components/{input/img/calendar → calendar/img}/chevron-right.svg +0 -0
- /package/src/components/{input/img/calendar → calendar/img}/chevron-up.svg +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
useDaumPostcodePopup,
|
|
6
|
+
type Address as DaumAddress,
|
|
7
|
+
} from "react-daum-postcode";
|
|
8
|
+
import { type FieldValues, type Path, useFormContext } from "react-hook-form";
|
|
9
|
+
|
|
10
|
+
import { createAddressSearchResult } from "../utils/address";
|
|
11
|
+
import type { AddressSelectionOptions } from "../types/address";
|
|
12
|
+
|
|
13
|
+
type FieldPath = Path<FieldValues>;
|
|
14
|
+
|
|
15
|
+
interface AddressFieldController {
|
|
16
|
+
formContext: ReturnType<typeof useFormContext>;
|
|
17
|
+
addressName?: FieldPath;
|
|
18
|
+
detailName?: FieldPath;
|
|
19
|
+
zipCodeName?: FieldPath;
|
|
20
|
+
setAddressValue: (value: string) => void;
|
|
21
|
+
setDetailValue: (value: string) => void;
|
|
22
|
+
clearDetailValue: () => void;
|
|
23
|
+
setZipCodeValue: (value: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const useAddressFieldController = ({
|
|
27
|
+
addressFieldName,
|
|
28
|
+
detailFieldName,
|
|
29
|
+
zipCodeFieldName,
|
|
30
|
+
triggerValidation,
|
|
31
|
+
}: Pick<
|
|
32
|
+
AddressSelectionOptions,
|
|
33
|
+
"addressFieldName" | "detailFieldName" | "zipCodeFieldName"
|
|
34
|
+
> & { triggerValidation: boolean }): AddressFieldController => {
|
|
35
|
+
const formContext = useFormContext<FieldValues>();
|
|
36
|
+
const addressName = addressFieldName
|
|
37
|
+
? (addressFieldName as FieldPath)
|
|
38
|
+
: undefined;
|
|
39
|
+
const detailName = detailFieldName
|
|
40
|
+
? (detailFieldName as FieldPath)
|
|
41
|
+
: undefined;
|
|
42
|
+
const zipCodeName = zipCodeFieldName
|
|
43
|
+
? (zipCodeFieldName as FieldPath)
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
const setAddressValue = useCallback(
|
|
47
|
+
(value: string) => {
|
|
48
|
+
if (!addressName) return;
|
|
49
|
+
formContext.setValue(addressName, value, {
|
|
50
|
+
shouldDirty: true,
|
|
51
|
+
shouldTouch: true,
|
|
52
|
+
shouldValidate: triggerValidation,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
[addressName, formContext, triggerValidation],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const setDetailValue = useCallback(
|
|
59
|
+
(value: string) => {
|
|
60
|
+
if (!detailName) return;
|
|
61
|
+
formContext.setValue(detailName, value, {
|
|
62
|
+
shouldDirty: true,
|
|
63
|
+
shouldTouch: true,
|
|
64
|
+
shouldValidate: triggerValidation,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
[detailName, formContext, triggerValidation],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const clearDetailValue = useCallback(() => {
|
|
71
|
+
setDetailValue("");
|
|
72
|
+
}, [setDetailValue]);
|
|
73
|
+
|
|
74
|
+
const setZipCodeValue = useCallback(
|
|
75
|
+
(value: string) => {
|
|
76
|
+
if (!zipCodeName) return;
|
|
77
|
+
formContext.setValue(zipCodeName, value, {
|
|
78
|
+
shouldDirty: true,
|
|
79
|
+
shouldTouch: true,
|
|
80
|
+
shouldValidate: triggerValidation,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
[formContext, triggerValidation, zipCodeName],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
formContext,
|
|
88
|
+
addressName,
|
|
89
|
+
detailName,
|
|
90
|
+
zipCodeName,
|
|
91
|
+
setAddressValue,
|
|
92
|
+
setDetailValue,
|
|
93
|
+
clearDetailValue,
|
|
94
|
+
setZipCodeValue,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 주소 검색 팝업을 열고 선택한 값을 react-hook-form과 콜백에 전달한다.
|
|
100
|
+
* @hook
|
|
101
|
+
* @param {AddressSelectionOptions} options 주소 필드 옵션
|
|
102
|
+
* @desc
|
|
103
|
+
* - return { openPopup, setAddressValue, setDetailValue, clearDetailValue, setZipCodeValue }
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* const { openPopup } = useAddress({
|
|
107
|
+
* addressFieldName: "farm.address",
|
|
108
|
+
* zipCodeFieldName: "farm.zipCode",
|
|
109
|
+
* onSelect: (payload) => console.log(payload.address),
|
|
110
|
+
* });
|
|
111
|
+
* <Input.Address.Button onClick={openPopup} />
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export const useAddress = ({
|
|
115
|
+
addressFieldName,
|
|
116
|
+
detailFieldName,
|
|
117
|
+
zipCodeFieldName,
|
|
118
|
+
triggerValidation = true,
|
|
119
|
+
resetDetailOnSelect = true,
|
|
120
|
+
onSelect,
|
|
121
|
+
}: AddressSelectionOptions) => {
|
|
122
|
+
const openDaumPopup = useDaumPostcodePopup();
|
|
123
|
+
const { setAddressValue, setDetailValue, clearDetailValue, setZipCodeValue } =
|
|
124
|
+
useAddressFieldController({
|
|
125
|
+
addressFieldName,
|
|
126
|
+
detailFieldName,
|
|
127
|
+
zipCodeFieldName,
|
|
128
|
+
triggerValidation,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const handleComplete = useCallback(
|
|
132
|
+
(address: DaumAddress) => {
|
|
133
|
+
const payload = createAddressSearchResult(address);
|
|
134
|
+
|
|
135
|
+
setAddressValue(payload.address);
|
|
136
|
+
setZipCodeValue(payload.zipCode);
|
|
137
|
+
if (resetDetailOnSelect) {
|
|
138
|
+
clearDetailValue();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onSelect?.(payload);
|
|
142
|
+
},
|
|
143
|
+
[
|
|
144
|
+
clearDetailValue,
|
|
145
|
+
onSelect,
|
|
146
|
+
resetDetailOnSelect,
|
|
147
|
+
setAddressValue,
|
|
148
|
+
setZipCodeValue,
|
|
149
|
+
],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const openPopup = useCallback(() => {
|
|
153
|
+
openDaumPopup({ onComplete: handleComplete });
|
|
154
|
+
}, [handleComplete, openDaumPopup]);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
/**
|
|
158
|
+
* 주소 검색 팝업 열기
|
|
159
|
+
*/
|
|
160
|
+
openPopup,
|
|
161
|
+
/**
|
|
162
|
+
* 주소 값을 외부에서 직접 갱신할 때 사용할 setter
|
|
163
|
+
*/
|
|
164
|
+
setAddressValue,
|
|
165
|
+
/**
|
|
166
|
+
* 상세 주소 값을 직접 갱신할 때 사용할 setter
|
|
167
|
+
*/
|
|
168
|
+
setDetailValue,
|
|
169
|
+
/**
|
|
170
|
+
* 상세 주소 값을 초기화한다.
|
|
171
|
+
*/
|
|
172
|
+
clearDetailValue,
|
|
173
|
+
/**
|
|
174
|
+
* 우편번호 값을 직접 갱신할 때 사용할 setter
|
|
175
|
+
*/
|
|
176
|
+
setZipCodeValue,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 주소/상세/우편번호 필드 값을 감시하고 제어 메서드를 제공한다.
|
|
182
|
+
* @hook
|
|
183
|
+
* @param {AddressSelectionOptions} options 주소 필드 옵션
|
|
184
|
+
* @desc
|
|
185
|
+
* - return { addressValue, detailValue, zipCodeValue, setAddressValue, setDetailValue, clearDetailValue, setZipCodeValue }
|
|
186
|
+
* @example
|
|
187
|
+
* ```tsx
|
|
188
|
+
* const {
|
|
189
|
+
* addressValue,
|
|
190
|
+
* detailValue,
|
|
191
|
+
* setDetailValue,
|
|
192
|
+
* } = useAddressFields({
|
|
193
|
+
* addressFieldName: "farm.address",
|
|
194
|
+
* detailFieldName: "farm.detailAddress",
|
|
195
|
+
* zipCodeFieldName: "farm.zipCode",
|
|
196
|
+
* });
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export const useAddressFields = ({
|
|
200
|
+
addressFieldName,
|
|
201
|
+
detailFieldName,
|
|
202
|
+
zipCodeFieldName,
|
|
203
|
+
triggerValidation = true,
|
|
204
|
+
}: AddressSelectionOptions) => {
|
|
205
|
+
const controller = useAddressFieldController({
|
|
206
|
+
addressFieldName,
|
|
207
|
+
detailFieldName,
|
|
208
|
+
zipCodeFieldName,
|
|
209
|
+
triggerValidation,
|
|
210
|
+
});
|
|
211
|
+
const {
|
|
212
|
+
formContext,
|
|
213
|
+
addressName,
|
|
214
|
+
detailName,
|
|
215
|
+
zipCodeName,
|
|
216
|
+
setAddressValue,
|
|
217
|
+
setDetailValue,
|
|
218
|
+
clearDetailValue,
|
|
219
|
+
setZipCodeValue,
|
|
220
|
+
} = controller;
|
|
221
|
+
|
|
222
|
+
const getStringValue = (value: unknown): string => {
|
|
223
|
+
if (typeof value === "string") return value;
|
|
224
|
+
if (value === undefined || value === null) return "";
|
|
225
|
+
return String(value);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const addressValue = addressName
|
|
229
|
+
? getStringValue(formContext.watch(addressName))
|
|
230
|
+
: "";
|
|
231
|
+
const detailValue = detailName
|
|
232
|
+
? getStringValue(formContext.watch(detailName))
|
|
233
|
+
: "";
|
|
234
|
+
const zipCodeValue = zipCodeName
|
|
235
|
+
? getStringValue(formContext.watch(zipCodeName))
|
|
236
|
+
: "";
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
addressValue,
|
|
240
|
+
detailValue,
|
|
241
|
+
zipCodeValue,
|
|
242
|
+
setAddressValue,
|
|
243
|
+
setDetailValue,
|
|
244
|
+
clearDetailValue,
|
|
245
|
+
setZipCodeValue,
|
|
246
|
+
};
|
|
247
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import { Button } from "../../../button";
|
|
6
|
+
import type { AddressFindButtonProps } from "../../types/address";
|
|
7
|
+
import { useAddress } from "../../hooks/useAddress";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 주소 찾기 버튼; react-daum-postcode 팝업을 열고 결과를 RHF와 콜백에 전달한다.
|
|
11
|
+
* @component
|
|
12
|
+
* @param {AddressFindButtonProps} props 버튼 props
|
|
13
|
+
* @param {React.ReactNode} [props.label] 버튼 라벨 텍스트
|
|
14
|
+
* @param {string} [props.className] 커스텀 className
|
|
15
|
+
* @param {"primary"|"secondary"|"tertiary"} [props.priority="secondary"] 버튼 priority
|
|
16
|
+
* @param {"solid"|"outlined"} [props.fill="solid"] 버튼 fill 타입
|
|
17
|
+
* @param {"xlarge"|"large"|"medium"|"small"} [props.size="xlarge"] 버튼 size
|
|
18
|
+
* @param {"button"|"submit"|"reset"} [props.type="button"] native type
|
|
19
|
+
* @param {boolean} [props.disabled] disabled 상태
|
|
20
|
+
* @param {string} [props.addressFieldName] react-hook-form 주소 필드 이름
|
|
21
|
+
* @param {string} [props.detailFieldName] react-hook-form 상세 주소 필드 이름
|
|
22
|
+
* @param {string} [props.zipCodeFieldName] react-hook-form 우편번호 필드 이름
|
|
23
|
+
* @param {boolean} [props.triggerValidation=true] setValue 이후 trigger 실행 여부
|
|
24
|
+
* @param {boolean} [props.resetDetailOnSelect=true] 새 주소 선택 시 상세 주소 초기화 여부
|
|
25
|
+
* @param {(payload:AddressSearchResult)=>void} [props.onSelect] 주소 선택 콜백
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Input.Address.Button
|
|
29
|
+
* label="검색"
|
|
30
|
+
* addressFieldName="farm.address"
|
|
31
|
+
* zipCodeFieldName="farm.zipCode"
|
|
32
|
+
* onSelect={(payload) => console.log(payload.address)}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export default function AddressFindButton({
|
|
37
|
+
label,
|
|
38
|
+
className,
|
|
39
|
+
priority = "secondary",
|
|
40
|
+
fill = "solid",
|
|
41
|
+
size = "xlarge",
|
|
42
|
+
type = "button",
|
|
43
|
+
disabled,
|
|
44
|
+
...selectionOptions
|
|
45
|
+
}: AddressFindButtonProps) {
|
|
46
|
+
// RHF + react-daum-postcode 연결을 위한 팝업 핸들러
|
|
47
|
+
const { openPopup } = useAddress(selectionOptions);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Button.Default
|
|
51
|
+
priority={priority}
|
|
52
|
+
fill={fill}
|
|
53
|
+
size={size}
|
|
54
|
+
type={type}
|
|
55
|
+
onClick={openPopup}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
className={clsx("input-address-button", className)}
|
|
58
|
+
>
|
|
59
|
+
{/* 비어 있는 라벨일 때 기본 문구를 강제로 노출 */}
|
|
60
|
+
{["string", "number"].includes(typeof label) && label !== ""
|
|
61
|
+
? String(label)
|
|
62
|
+
: (label ?? "검색")}
|
|
63
|
+
</Button.Default>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { useFormContext, type FieldValues, type Path } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import InputBase from "../foundation/Input";
|
|
8
|
+
import type { AddressTemplateProps } from "../../types/address";
|
|
9
|
+
import AddressFindButton from "./Button";
|
|
10
|
+
import { resolveAddressValue } from "../../utils/address";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 주소 입력 템플릿; 주소/상세 입력 필드와 검색 버튼을 조합한다.
|
|
14
|
+
* @component
|
|
15
|
+
* @param {AddressTemplateProps} props 템플릿 props
|
|
16
|
+
* @param {string} [props.className] container className
|
|
17
|
+
* @param {AddressSharedInputStyleProps} [props.inputStyle] 공유 input 스타일 옵션
|
|
18
|
+
* @param {InputProps} props.addressInput 주소 입력 필드 props
|
|
19
|
+
* @param {InputProps} [props.detailInput] 상세 주소 입력 필드 props
|
|
20
|
+
* @param {AddressTemplateButtonOptions} [props.buttonProps] 검색 버튼 옵션
|
|
21
|
+
* @param {boolean} [props.disabled] 템플릿 disabled 상태
|
|
22
|
+
* @param {string} [props.addressFieldName] RHF 주소 필드 네임
|
|
23
|
+
* @param {string} [props.zipCodeFieldName] RHF 우편번호 필드 네임
|
|
24
|
+
* @param {boolean} [props.triggerValidation=true] setValue 이후 trigger 실행 여부
|
|
25
|
+
* @param {(payload:AddressSearchResult)=>void} [props.onSelect] 주소 선택 콜백
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <FormProvider {...methods}>
|
|
29
|
+
* <Input.Address.Template
|
|
30
|
+
* addressFieldName="farm.address"
|
|
31
|
+
* zipCodeFieldName="farm.zipCode"
|
|
32
|
+
* addressInput={{ placeholder: "주소 검색" }}
|
|
33
|
+
* detailInput={{ placeholder: "상세 주소 입력" }}
|
|
34
|
+
* buttonProps={{ label: "검색" }}
|
|
35
|
+
* />
|
|
36
|
+
* </FormProvider>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export default function AddressFieldTemplate({
|
|
40
|
+
className,
|
|
41
|
+
addressInput,
|
|
42
|
+
detailInput,
|
|
43
|
+
inputStyle,
|
|
44
|
+
buttonProps,
|
|
45
|
+
disabled,
|
|
46
|
+
addressFieldName,
|
|
47
|
+
detailFieldName,
|
|
48
|
+
zipCodeFieldName,
|
|
49
|
+
triggerValidation,
|
|
50
|
+
resetDetailOnSelect = true,
|
|
51
|
+
onSelect,
|
|
52
|
+
}: AddressTemplateProps) {
|
|
53
|
+
const formContext = useFormContext<FieldValues>();
|
|
54
|
+
const inputSize = inputStyle?.size ?? "medium";
|
|
55
|
+
|
|
56
|
+
const resolvedAddressFieldName = addressFieldName
|
|
57
|
+
? (addressFieldName as Path<FieldValues>)
|
|
58
|
+
: undefined;
|
|
59
|
+
const resolvedZipFieldName = zipCodeFieldName
|
|
60
|
+
? (zipCodeFieldName as Path<FieldValues>)
|
|
61
|
+
: undefined;
|
|
62
|
+
const resolvedDetailFieldName = detailFieldName
|
|
63
|
+
? (detailFieldName as Path<FieldValues>)
|
|
64
|
+
: detailInput?.name
|
|
65
|
+
? (detailInput.name as Path<FieldValues>)
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (resolvedAddressFieldName)
|
|
70
|
+
formContext.register(resolvedAddressFieldName);
|
|
71
|
+
if (resolvedZipFieldName) formContext.register(resolvedZipFieldName);
|
|
72
|
+
}, [formContext, resolvedAddressFieldName, resolvedZipFieldName]);
|
|
73
|
+
|
|
74
|
+
const watchedAddress = resolvedAddressFieldName
|
|
75
|
+
? formContext.watch(resolvedAddressFieldName)
|
|
76
|
+
: undefined;
|
|
77
|
+
// addressInput.value가 지정되면 RHF watch보다 우선 적용해 강제 값 주입을 허용한다.
|
|
78
|
+
const addressValueSource =
|
|
79
|
+
addressInput?.value !== undefined ? addressInput.value : watchedAddress;
|
|
80
|
+
const resolvedAddress = resolveAddressValue(addressValueSource);
|
|
81
|
+
// detailFieldName 지정 시 RHF register를 자동으로 연결한다.
|
|
82
|
+
const detailRegister = resolvedDetailFieldName
|
|
83
|
+
? formContext.register(resolvedDetailFieldName)
|
|
84
|
+
: detailInput?.register;
|
|
85
|
+
const detailName =
|
|
86
|
+
detailInput?.name ?? detailFieldName ?? resolvedDetailFieldName;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className={clsx("input-address-container", className)}>
|
|
90
|
+
<div className="input-address-row input-address-upper">
|
|
91
|
+
<InputBase
|
|
92
|
+
{...inputStyle}
|
|
93
|
+
{...addressInput}
|
|
94
|
+
size={inputSize}
|
|
95
|
+
value={resolvedAddress.text}
|
|
96
|
+
width="fill"
|
|
97
|
+
readOnly
|
|
98
|
+
disabled
|
|
99
|
+
className="input-address-field input-address-field-base"
|
|
100
|
+
/>
|
|
101
|
+
<AddressFindButton
|
|
102
|
+
{...buttonProps}
|
|
103
|
+
addressFieldName={addressFieldName}
|
|
104
|
+
detailFieldName={detailFieldName ?? detailInput?.name}
|
|
105
|
+
zipCodeFieldName={zipCodeFieldName}
|
|
106
|
+
triggerValidation={triggerValidation}
|
|
107
|
+
resetDetailOnSelect={resetDetailOnSelect}
|
|
108
|
+
onSelect={onSelect}
|
|
109
|
+
className="input-address-button"
|
|
110
|
+
size={
|
|
111
|
+
buttonProps?.size ??
|
|
112
|
+
(inputSize === "large"
|
|
113
|
+
? "xlarge"
|
|
114
|
+
: inputSize === "medium"
|
|
115
|
+
? "large"
|
|
116
|
+
: "medium")
|
|
117
|
+
}
|
|
118
|
+
disabled={disabled ?? buttonProps?.disabled}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
{detailInput && (
|
|
122
|
+
<div className="input-address-row input-address-lower">
|
|
123
|
+
<InputBase
|
|
124
|
+
{...inputStyle}
|
|
125
|
+
{...detailInput}
|
|
126
|
+
register={detailRegister}
|
|
127
|
+
name={detailName}
|
|
128
|
+
width="full"
|
|
129
|
+
className="input-address-field input-address-field-detail"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ChangeEvent, MouseEvent as ReactMouseEvent } from "react";
|
|
4
|
+
import { forwardRef, useCallback, useMemo } from "react";
|
|
5
|
+
import { useUncontrolled } from "@mantine/hooks";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import { Calendar } from "../../../calendar";
|
|
8
|
+
import type {
|
|
9
|
+
InputCalendarProps,
|
|
10
|
+
InputCalendarTriggerRenderProps,
|
|
11
|
+
} from "../../types";
|
|
12
|
+
import type { CalendarValue } from "../../../calendar";
|
|
13
|
+
import {
|
|
14
|
+
createEmptyValue,
|
|
15
|
+
formatTriggerValue,
|
|
16
|
+
getTodayValue,
|
|
17
|
+
serializeCalendarValue,
|
|
18
|
+
} from "../../utils/date";
|
|
19
|
+
import InputDateFooterTemplate from "./footer/Template";
|
|
20
|
+
import InputDateTrigger from "./Trigger";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Input Date Template; trigger + calendar 조합.
|
|
24
|
+
* @component
|
|
25
|
+
* @param {InputCalendarProps} props
|
|
26
|
+
* @param {CalendarMode} [props.mode="date"] 날짜/시간 모드
|
|
27
|
+
* @param {CalendarColumns} [props.columns=1] 동시 노출 달력 수
|
|
28
|
+
* @param {CalendarValue} [props.value] 제어형 값
|
|
29
|
+
* @param {CalendarValue} [props.defaultValue] 비제어 초기값
|
|
30
|
+
* @param {CalendarOnChange} [props.onChange] 값 변경 이벤트
|
|
31
|
+
* @param {CalendarOnChange} [props.onValueChange] onChange alias
|
|
32
|
+
* @param {boolean} [props.readOnly] 읽기 전용 여부
|
|
33
|
+
* @param {boolean} [props.disabled] 비활성화 여부
|
|
34
|
+
* @param {CalendarDatePickerProps} [props.datePickerProps] Mantine DatePicker 옵션
|
|
35
|
+
* @param {string} [props.name] form name/RHF name
|
|
36
|
+
* @param {UseFormRegisterReturn} [props.register] RHF register
|
|
37
|
+
* @param {string} [props.placeholder="YYYY-MM-DD"] placeholder
|
|
38
|
+
* @param {ReactNode} [props.header] 패널 header 콘텐츠
|
|
39
|
+
* @param {ReactNode} [props.footer] 패널 footer 콘텐츠
|
|
40
|
+
* @param {unknown} [props.timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
|
|
41
|
+
* @param {boolean} [props.calendarOpened] calendar 열림 제어 상태
|
|
42
|
+
* @param {(event: MouseEvent<Element>) => void} [props.onClick] trigger 클릭 핸들러
|
|
43
|
+
* @param {string} [props.className] root className
|
|
44
|
+
* @param {string} [props.id] trigger id
|
|
45
|
+
* @param {ReactNode} [props.trigger] 커스텀 trigger 슬롯
|
|
46
|
+
* @param {(props: InputCalendarTriggerRenderProps) => ReactNode} [props.renderTrigger] 커스텀 trigger 렌더 함수
|
|
47
|
+
*/
|
|
48
|
+
const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
|
|
49
|
+
(
|
|
50
|
+
{
|
|
51
|
+
mode = "date",
|
|
52
|
+
columns = 1,
|
|
53
|
+
value,
|
|
54
|
+
defaultValue,
|
|
55
|
+
onChange,
|
|
56
|
+
onValueChange,
|
|
57
|
+
readOnly,
|
|
58
|
+
disabled,
|
|
59
|
+
datePickerProps,
|
|
60
|
+
name,
|
|
61
|
+
register,
|
|
62
|
+
placeholder = "YYYY-MM-DD",
|
|
63
|
+
className,
|
|
64
|
+
header,
|
|
65
|
+
footer,
|
|
66
|
+
calendarOpened,
|
|
67
|
+
onClick: triggerOnClick,
|
|
68
|
+
id,
|
|
69
|
+
trigger,
|
|
70
|
+
renderTrigger,
|
|
71
|
+
},
|
|
72
|
+
ref,
|
|
73
|
+
) => {
|
|
74
|
+
// useUncontrolled로 제어형/비제어 값을 모두 허용한다.
|
|
75
|
+
const [calendarValue, setCalendarValue] = useUncontrolled<CalendarValue>({
|
|
76
|
+
value,
|
|
77
|
+
defaultValue,
|
|
78
|
+
finalValue: createEmptyValue(),
|
|
79
|
+
onChange: next => {
|
|
80
|
+
onChange?.(next as CalendarValue);
|
|
81
|
+
onValueChange?.(next as CalendarValue);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// react-hook-form register onChange를 수동 호출해 값 직렬화를 맞춘다.
|
|
86
|
+
const emitRegisterChange = useCallback(
|
|
87
|
+
(nextValue: CalendarValue) => {
|
|
88
|
+
if (!register?.onChange) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const serialized = serializeCalendarValue(nextValue);
|
|
92
|
+
const syntheticEvent = {
|
|
93
|
+
target: {
|
|
94
|
+
name: register.name,
|
|
95
|
+
value: serialized,
|
|
96
|
+
},
|
|
97
|
+
currentTarget: {
|
|
98
|
+
name: register.name,
|
|
99
|
+
value: serialized,
|
|
100
|
+
},
|
|
101
|
+
} as unknown as ChangeEvent<HTMLInputElement>;
|
|
102
|
+
register.onChange(syntheticEvent);
|
|
103
|
+
},
|
|
104
|
+
[register],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const updateValue = useCallback(
|
|
108
|
+
(nextValue: CalendarValue) => {
|
|
109
|
+
setCalendarValue(nextValue);
|
|
110
|
+
emitRegisterChange(nextValue);
|
|
111
|
+
},
|
|
112
|
+
[emitRegisterChange, setCalendarValue],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const triggerValue = useMemo(
|
|
116
|
+
() => formatTriggerValue(calendarValue) ?? "",
|
|
117
|
+
[calendarValue],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleTriggerClick = (event: ReactMouseEvent<Element>) => {
|
|
121
|
+
triggerOnClick?.(event);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleCalendarChange = (nextValue: CalendarValue) => {
|
|
125
|
+
updateValue(nextValue);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// footer 미지정 시 기본 FooterTemplate을 연결한다.
|
|
129
|
+
const footerContent = footer ?? (
|
|
130
|
+
<InputDateFooterTemplate
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
onClear={() => updateValue(createEmptyValue())}
|
|
133
|
+
onToday={() => updateValue(getTodayValue())}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const triggerRenderProps: InputCalendarTriggerRenderProps = {
|
|
138
|
+
id,
|
|
139
|
+
name: name ?? register?.name,
|
|
140
|
+
placeholder,
|
|
141
|
+
displayValue: triggerValue,
|
|
142
|
+
disabled,
|
|
143
|
+
onClick: handleTriggerClick,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const triggerNode = trigger ?? renderTrigger?.(triggerRenderProps) ?? (
|
|
147
|
+
<InputDateTrigger
|
|
148
|
+
id={id}
|
|
149
|
+
name={name ?? register?.name}
|
|
150
|
+
register={register}
|
|
151
|
+
placeholder={placeholder}
|
|
152
|
+
displayValue={triggerValue}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
onClick={handleTriggerClick}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Calendar.Root
|
|
160
|
+
ref={ref}
|
|
161
|
+
className={clsx("input-date-field", className)}
|
|
162
|
+
mode={mode}
|
|
163
|
+
columns={columns}
|
|
164
|
+
disabled={disabled}
|
|
165
|
+
readOnly={readOnly}
|
|
166
|
+
value={calendarValue}
|
|
167
|
+
onChange={handleCalendarChange}
|
|
168
|
+
datePickerProps={datePickerProps}
|
|
169
|
+
header={header}
|
|
170
|
+
footer={footerContent}
|
|
171
|
+
open={calendarOpened}
|
|
172
|
+
>
|
|
173
|
+
{triggerNode}
|
|
174
|
+
</Calendar.Root>
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
InputDateTemplate.displayName = "InputDateTemplate";
|
|
180
|
+
|
|
181
|
+
export default InputDateTemplate;
|