@uniai-fe/uds-primitives 0.2.0 → 0.2.2

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.
Files changed (44) hide show
  1. package/dist/styles.css +105 -13
  2. package/package.json +5 -3
  3. package/src/components/button/index.tsx +0 -2
  4. package/src/components/button/markup/Base.tsx +22 -1
  5. package/src/components/button/styles/button.scss +24 -2
  6. package/src/components/button/styles/variables.scss +4 -0
  7. package/src/components/button/types/index.ts +7 -0
  8. package/src/components/checkbox/markup/Checkbox.tsx +31 -25
  9. package/src/components/dropdown/markup/Template.tsx +4 -8
  10. package/src/components/dropdown/markup/foundation/Container.tsx +35 -7
  11. package/src/components/dropdown/markup/foundation/MenuItem.tsx +10 -10
  12. package/src/components/dropdown/markup/index.tsx +10 -1
  13. package/src/components/dropdown/styles/dropdown.scss +2 -2
  14. package/src/components/dropdown/styles/variables.scss +4 -4
  15. package/src/components/dropdown/types/base.ts +13 -0
  16. package/src/components/dropdown/types/props.ts +23 -27
  17. package/src/components/input/hooks/index.ts +1 -0
  18. package/src/components/input/hooks/useAddress.ts +247 -0
  19. package/src/components/input/index.scss +5 -1
  20. package/src/components/input/markup/address/Button.tsx +65 -0
  21. package/src/components/input/markup/address/Template.tsx +135 -0
  22. package/src/components/input/markup/address/index.ts +9 -0
  23. package/src/components/input/markup/foundation/Input.tsx +20 -1
  24. package/src/components/input/markup/index.tsx +2 -0
  25. package/src/components/input/styles/address.scss +24 -0
  26. package/src/components/input/styles/foundation.scss +28 -2
  27. package/src/components/input/styles/variables.scss +4 -0
  28. package/src/components/input/types/address.ts +249 -0
  29. package/src/components/input/types/foundation.ts +6 -0
  30. package/src/components/input/types/index.ts +1 -0
  31. package/src/components/input/utils/address.ts +165 -0
  32. package/src/components/input/utils/index.tsx +1 -0
  33. package/src/components/radio/markup/Radio.tsx +10 -2
  34. package/src/components/radio/markup/RadioCard.tsx +6 -1
  35. package/src/components/radio/markup/RadioCardGroup.tsx +6 -1
  36. package/src/components/select/markup/Default.tsx +6 -4
  37. package/src/components/select/markup/foundation/Container.tsx +23 -0
  38. package/src/components/select/markup/multiple/Multiple.tsx +6 -4
  39. package/src/components/select/styles/select.scss +25 -2
  40. package/src/components/select/styles/variables.scss +4 -0
  41. package/src/components/select/types/index.ts +1 -0
  42. package/src/components/select/types/option.ts +43 -0
  43. package/src/components/select/types/props.ts +29 -9
  44. package/src/components/input/styles/index.scss +0 -4
@@ -6,23 +6,22 @@ import type {
6
6
  import type { HTMLAttributes, MutableRefObject, ReactNode } from "react";
7
7
 
8
8
  import type { CheckboxProps } from "../../checkbox/types";
9
- import type { DropdownRootProps, DropdownSize } from "./base";
9
+ import type {
10
+ DropdownPanelWidth,
11
+ DropdownRootProps,
12
+ DropdownSize,
13
+ } from "./base";
10
14
 
11
15
  /**
12
16
  * Dropdown trigger props
13
17
  * @property {boolean} [asChild=true] trigger를 asChild 패턴으로 감쌀지 여부
14
18
  */
15
- export interface DropdownTriggerProps extends DropdownMenuTriggerProps {
16
- /**
17
- * trigger를 asChild 패턴으로 감쌀지 여부
18
- */
19
- asChild?: DropdownMenuTriggerProps["asChild"];
20
- }
19
+ export type DropdownTriggerProps = DropdownMenuTriggerProps;
21
20
 
22
21
  /**
23
22
  * Dropdown Container props
24
23
  * @property {DropdownSize} [size="medium"] option 높이 스케일
25
- * @property {boolean} [matchTriggerWidth=true] trigger 너비에 맞춰 dropdown 너비를 맞출지 여부
24
+ * @property {DropdownPanelWidth} [width="match"] dropdown panel width 옵션
26
25
  * @property {HTMLElement | null} [portalContainer] portal을 렌더링할 DOM 컨테이너
27
26
  */
28
27
  export interface DropdownContainerProps extends DropdownMenuContentProps {
@@ -33,7 +32,7 @@ export interface DropdownContainerProps extends DropdownMenuContentProps {
33
32
  /**
34
33
  * trigger 너비에 맞춰 dropdown 너비를 맞출지 여부
35
34
  */
36
- matchTriggerWidth?: boolean;
35
+ width?: DropdownPanelWidth;
37
36
  /**
38
37
  * portal을 렌더링할 DOM 컨테이너
39
38
  */
@@ -44,8 +43,8 @@ export interface DropdownContainerProps extends DropdownMenuContentProps {
44
43
  * Dropdown menu item props
45
44
  * @property {ReactNode} [label] 옵션 라벨
46
45
  * @property {ReactNode} [description] 보조 텍스트
47
- * @property {ReactNode} [leftSlot] 좌측 슬롯
48
- * @property {ReactNode} [rightSlot] 우측 슬롯
46
+ * @property {ReactNode} [left] 좌측 콘텐츠
47
+ * @property {ReactNode} [right] 우측 콘텐츠
49
48
  * @property {boolean} [isSelected] 선택 상태 여부
50
49
  * @property {boolean} [multiple] multi select 스타일 여부
51
50
  * @property {CheckboxProps} [checkboxProps] multiple 시 Checkbox 커스터마이징 옵션
@@ -60,13 +59,13 @@ export interface DropdownMenuItemProps extends RadixDropdownMenuItemProps {
60
59
  */
61
60
  description?: ReactNode;
62
61
  /**
63
- * 좌측 슬롯
62
+ * 좌측 콘텐츠
64
63
  */
65
- leftSlot?: ReactNode;
64
+ left?: ReactNode;
66
65
  /**
67
- * 우측 슬롯
66
+ * 우측 콘텐츠
68
67
  */
69
- rightSlot?: ReactNode;
68
+ right?: ReactNode;
70
69
  /**
71
70
  * 선택 상태 여부
72
71
  */
@@ -103,8 +102,8 @@ export interface DropdownContextValue {
103
102
  * @property {ReactNode} label 옵션 라벨
104
103
  * @property {ReactNode} [description] 보조 텍스트
105
104
  * @property {boolean} [disabled] 비활성 여부
106
- * @property {ReactNode} [leftSlot] 좌측 슬롯
107
- * @property {ReactNode} [rightSlot] 우측 슬롯
105
+ * @property {ReactNode} [left] 좌측 콘텐츠
106
+ * @property {ReactNode} [right] 우측 콘텐츠
108
107
  * @property {boolean} [multiple] multi select 스타일 여부
109
108
  */
110
109
  export interface DropdownTemplateItem {
@@ -125,13 +124,13 @@ export interface DropdownTemplateItem {
125
124
  */
126
125
  disabled?: boolean;
127
126
  /**
128
- * 좌측 슬롯
127
+ * 좌측 콘텐츠
129
128
  */
130
- leftSlot?: ReactNode;
129
+ left?: ReactNode;
131
130
  /**
132
- * 우측 슬롯
131
+ * 우측 콘텐츠
133
132
  */
134
- rightSlot?: ReactNode;
133
+ right?: ReactNode;
135
134
  /**
136
135
  * multi select 스타일 여부
137
136
  */
@@ -144,7 +143,7 @@ export interface DropdownTemplateItem {
144
143
  * @property {DropdownTemplateItem[]} items 렌더링할 menu item 리스트
145
144
  * @property {string[]} [selectedIds] 선택된 item id 배열
146
145
  * @property {DropdownSize} [size="medium"] surface height scale
147
- * @property {boolean} [matchTriggerWidth=true] trigger width 동기화 여부
146
+ * @property {DropdownPanelWidth} [width="match"] panel width 옵션
148
147
  * @property {DropdownRootProps} [rootProps] Root 에 전달할 props
149
148
  * @property {DropdownContainerProps} [containerProps] Container 에 전달할 props
150
149
  * @property {DropdownMenuListProps} [menuListProps] MenuList 에 전달할 props
@@ -155,7 +154,7 @@ export interface DropdownTemplateProps {
155
154
  selectedIds?: string[];
156
155
  onSelect?: (item: DropdownTemplateItem) => void;
157
156
  size?: DropdownSize;
158
- matchTriggerWidth?: boolean;
157
+ width?: DropdownPanelWidth;
159
158
  /**
160
159
  * Root 에 전달할 props
161
160
  */
@@ -163,10 +162,7 @@ export interface DropdownTemplateProps {
163
162
  /**
164
163
  * Container 에 전달할 props
165
164
  */
166
- containerProps?: Omit<
167
- DropdownContainerProps,
168
- "children" | "size" | "matchTriggerWidth"
169
- >;
165
+ containerProps?: Omit<DropdownContainerProps, "children" | "size" | "width">;
170
166
  /**
171
167
  * MenuList 에 전달할 props
172
168
  */
@@ -1 +1,2 @@
1
1
  export { useDigitField } from "./useDigitField";
2
+ export { useAddress, useAddressFields } from "./useAddress";
@@ -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
+ };
@@ -1 +1,5 @@
1
- @use "./styles/index.scss";
1
+ @use "./styles/variables.scss" as inputVariables;
2
+ @use "./styles/foundation.scss" as inputFoundation;
3
+ @use "./styles/text.scss" as inputText;
4
+ @use "./styles/calendar.scss" as inputCalendar;
5
+ @use "./styles/address.scss" as inputAddress;
@@ -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,9 @@
1
+ "use client";
2
+
3
+ import AddressFindButton from "./Button";
4
+ import AddressFieldTemplate from "./Template";
5
+
6
+ export const InputAddress = {
7
+ Button: AddressFindButton,
8
+ Template: AddressFieldTemplate,
9
+ };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import clsx from "clsx";
4
- import type { PointerEvent as ReactPointerEvent } from "react";
4
+ import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react";
5
5
  import {
6
6
  ChangeEvent,
7
7
  FocusEvent,
@@ -14,6 +14,10 @@ import {
14
14
  useState,
15
15
  } from "react";
16
16
  import type { InputProps } from "../../types";
17
+ import {
18
+ getFormFieldWidthAttr,
19
+ getFormFieldWidthValue,
20
+ } from "../../../form/utils/form-field";
17
21
 
18
22
  import InputBaseSideSlot from "./SideSlot";
19
23
  import InputBaseUtil from "./Utility";
@@ -56,6 +60,7 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
56
60
  size = "medium",
57
61
  state: stateProp = "default",
58
62
  block = false,
63
+ width,
59
64
  left,
60
65
  right,
61
66
  clear,
@@ -166,6 +171,18 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
166
171
  };
167
172
 
168
173
  const inputName = registerName ?? name;
174
+ const widthAttr =
175
+ width !== undefined
176
+ ? getFormFieldWidthAttr(width)
177
+ : block
178
+ ? "full"
179
+ : undefined;
180
+ const widthValue =
181
+ width !== undefined ? getFormFieldWidthValue(width) : undefined;
182
+ const containerStyle: CSSProperties | undefined =
183
+ widthValue !== undefined
184
+ ? ({ ["--input-width" as const]: widthValue } as CSSProperties)
185
+ : undefined;
169
186
 
170
187
  return (
171
188
  <div
@@ -182,6 +199,8 @@ const InputBase = forwardRef<HTMLInputElement, InputProps>(
182
199
  data-state={visualState}
183
200
  data-block={block ? "true" : undefined}
184
201
  {...(simulatedState ? { "data-simulated-state": simulatedState } : {})}
202
+ data-width={widthAttr}
203
+ style={containerStyle}
185
204
  >
186
205
  <div
187
206
  className={clsx(
@@ -3,9 +3,11 @@
3
3
  import { InputFoundation } from "./foundation";
4
4
  import { InputText } from "./text";
5
5
  import { InputCalendar } from "./calendar";
6
+ import { InputAddress } from "./address";
6
7
 
7
8
  export const Input = {
8
9
  ...InputFoundation,
9
10
  Text: InputText,
10
11
  Calendar: InputCalendar,
12
+ Address: InputAddress,
11
13
  };
@@ -0,0 +1,24 @@
1
+ .input-address-container {
2
+ width: 100%;
3
+ }
4
+
5
+ .input-address-row {
6
+ width: 100%;
7
+ display: flex;
8
+ gap: var(--spacing-gap-5);
9
+ }
10
+
11
+ .input-address-lower {
12
+ margin-top: var(--spacing-gap-5);
13
+ }
14
+
15
+ // .input-address-button {
16
+ // }
17
+
18
+ .input-address-upper {
19
+ align-items: center;
20
+ }
21
+
22
+ .input-address-field {
23
+ width: 100%;
24
+ }