@uniai-fe/uds-primitives 0.2.1 → 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 (35) hide show
  1. package/dist/styles.css +99 -7
  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/index.tsx +10 -1
  10. package/src/components/input/hooks/index.ts +1 -0
  11. package/src/components/input/hooks/useAddress.ts +247 -0
  12. package/src/components/input/index.scss +5 -1
  13. package/src/components/input/markup/address/Button.tsx +65 -0
  14. package/src/components/input/markup/address/Template.tsx +135 -0
  15. package/src/components/input/markup/address/index.ts +9 -0
  16. package/src/components/input/markup/foundation/Input.tsx +20 -1
  17. package/src/components/input/markup/index.tsx +2 -0
  18. package/src/components/input/styles/address.scss +24 -0
  19. package/src/components/input/styles/foundation.scss +28 -2
  20. package/src/components/input/styles/variables.scss +4 -0
  21. package/src/components/input/types/address.ts +249 -0
  22. package/src/components/input/types/foundation.ts +6 -0
  23. package/src/components/input/types/index.ts +1 -0
  24. package/src/components/input/utils/address.ts +165 -0
  25. package/src/components/input/utils/index.tsx +1 -0
  26. package/src/components/radio/markup/Radio.tsx +10 -2
  27. package/src/components/radio/markup/RadioCard.tsx +6 -1
  28. package/src/components/radio/markup/RadioCardGroup.tsx +6 -1
  29. package/src/components/select/markup/Default.tsx +2 -0
  30. package/src/components/select/markup/foundation/Container.tsx +23 -0
  31. package/src/components/select/markup/multiple/Multiple.tsx +2 -0
  32. package/src/components/select/styles/select.scss +25 -2
  33. package/src/components/select/styles/variables.scss +4 -0
  34. package/src/components/select/types/props.ts +24 -5
  35. package/src/components/input/styles/index.scss +0 -4
package/dist/styles.css CHANGED
@@ -11,6 +11,9 @@
11
11
  --theme-badge-line-height: var(--font-caption-medium-line-height, 1.5);
12
12
  --theme-badge-letter-spacing: var(--font-caption-medium-letter-spacing, 0);
13
13
  --theme-badge-dot-size: var(--spacing-gap-3, 8px);
14
+ /* layout presets */
15
+ --button-width: fit-content;
16
+ --button-flex: 0 0 auto;
14
17
  /* default button spacing (size 기반) */
15
18
  --button-default-gap-small: var(--spacing-gap-1);
16
19
  --button-default-gap-medium: var(--spacing-gap-2);
@@ -225,6 +228,9 @@
225
228
  --dropdown-option-height-small: var(--theme-size-medium-1, 40px);
226
229
  --dropdown-option-height-medium: var(--theme-size-medium-2, 48px);
227
230
  --dropdown-option-height-large: var(--theme-size-medium-3, 56px);
231
+ /* Layout presets */
232
+ --input-width: 100%;
233
+ --input-flex: 0 1 auto;
228
234
  /* Input sizing tokens; Button 변수 규칙과 동일한 prefix 패턴을 맞춘다. */
229
235
  --input-default-height-small: var(--theme-size-medium-1);
230
236
  --input-default-height-medium: var(--theme-size-medium-2);
@@ -260,6 +266,9 @@
260
266
  --input-surface-color: var(--color-common-100);
261
267
  --input-surface-muted-color: var(--color-neutral-99);
262
268
  --input-surface-disabled-color: var(--color-neutral-95);
269
+ /* layout presets */
270
+ --select-width: 100%;
271
+ --select-flex: 0 1 auto;
263
272
  --select-primary-height-small: var(--input-default-height-small);
264
273
  --select-primary-height-medium: var(--input-default-height-medium);
265
274
  --select-primary-height-large: var(--input-default-height-large);
@@ -644,7 +653,8 @@
644
653
  align-items: center;
645
654
  justify-content: center;
646
655
  gap: var(--button-default-gap-medium, var(--spacing-gap-2, 8px));
647
- width: fit-content;
656
+ width: var(--button-width);
657
+ flex: var(--button-flex);
648
658
  min-width: var(--button-default-width-min-base, var(--theme-size-small-2, 24px));
649
659
  min-height: var(--button-min-height, auto);
650
660
  padding-inline: var(--button-padding-inline, var(--button-default-padding-inline-base, var(--spacing-padding-4, 16px)));
@@ -672,8 +682,24 @@
672
682
  align-items: center;
673
683
  justify-content: center;
674
684
  }
675
- .button.button-block {
676
- width: 100%;
685
+ .button[data-width=auto] {
686
+ --button-width: auto;
687
+ --button-flex: 0 1 auto;
688
+ }
689
+ .button[data-width=fill] {
690
+ --button-width: auto;
691
+ --button-flex: 1 1 0%;
692
+ }
693
+ .button[data-width=full], .button.button-block {
694
+ --button-width: 100%;
695
+ --button-flex: 0 0 100%;
696
+ }
697
+ .button[data-width=fit] {
698
+ --button-width: fit-content;
699
+ --button-flex: 0 0 auto;
700
+ }
701
+ .button[data-width=custom] {
702
+ --button-flex: 0 0 auto;
677
703
  }
678
704
  .button:not(.button-fill-solid):not(.button-fill-outlined) {
679
705
  background-color: transparent;
@@ -1603,9 +1629,30 @@ figure.chip {
1603
1629
  display: flex;
1604
1630
  flex-direction: column;
1605
1631
  gap: var(--spacing-gap-3);
1606
- width: 100%;
1632
+ width: var(--input-width);
1633
+ flex: var(--input-flex);
1634
+ min-width: 0;
1635
+ }
1636
+ .input[data-width=auto] {
1637
+ --input-width: auto;
1638
+ --input-flex: 0 1 auto;
1639
+ }
1640
+ .input[data-width=fill] {
1641
+ --input-width: auto;
1642
+ --input-flex: 1 1 0%;
1607
1643
  }
1608
- .input[data-block=true], .input--block {
1644
+ .input[data-width=full], .input[data-block=true] {
1645
+ --input-width: 100%;
1646
+ --input-flex: 0 0 100%;
1647
+ }
1648
+ .input[data-width=fit] {
1649
+ --input-width: fit-content;
1650
+ --input-flex: 0 0 auto;
1651
+ }
1652
+ .input[data-width=custom] {
1653
+ --input-flex: 0 0 auto;
1654
+ }
1655
+ .input--block {
1609
1656
  width: 100%;
1610
1657
  }
1611
1658
 
@@ -2079,18 +2126,63 @@ figure.chip {
2079
2126
  width: 100%;
2080
2127
  }
2081
2128
 
2129
+ .input-address-container {
2130
+ width: 100%;
2131
+ }
2132
+
2133
+ .input-address-row {
2134
+ width: 100%;
2135
+ display: flex;
2136
+ gap: var(--spacing-gap-5);
2137
+ }
2138
+
2139
+ .input-address-lower {
2140
+ margin-top: var(--spacing-gap-5);
2141
+ }
2142
+
2143
+ .input-address-upper {
2144
+ align-items: center;
2145
+ }
2146
+
2147
+ .input-address-field {
2148
+ width: 100%;
2149
+ }
2150
+
2082
2151
  /* Select tokens mapped to Input primary tokens for visual parity */
2083
2152
 
2084
2153
 
2085
2154
  .select {
2086
2155
  display: flex;
2087
- width: 100%;
2156
+ width: var(--select-width);
2157
+ flex: var(--select-flex);
2088
2158
  flex-direction: column;
2089
2159
  gap: var(--spacing-gap-2);
2160
+ min-width: 0;
2161
+ }
2162
+
2163
+ .select[data-width=auto] {
2164
+ --select-width: auto;
2165
+ --select-flex: 0 1 auto;
2166
+ }
2167
+
2168
+ .select[data-width=fill] {
2169
+ --select-width: auto;
2170
+ --select-flex: 1 1 0%;
2090
2171
  }
2091
2172
 
2173
+ .select[data-width=full],
2092
2174
  .select-block {
2093
- width: 100%;
2175
+ --select-width: 100%;
2176
+ --select-flex: 0 0 100%;
2177
+ }
2178
+
2179
+ .select[data-width=fit] {
2180
+ --select-width: fit-content;
2181
+ --select-flex: 0 0 auto;
2182
+ }
2183
+
2184
+ .select[data-width=custom] {
2185
+ --select-flex: 0 0 auto;
2094
2186
  }
2095
2187
 
2096
2188
  .select-button {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -58,7 +58,8 @@
58
58
  "@uniai-fe/util-functions": "^0.2.3",
59
59
  "react": "^19",
60
60
  "react-dom": "^19",
61
- "react-hook-form": "^7"
61
+ "react-hook-form": "^7",
62
+ "react-daum-postcode": "^3"
62
63
  },
63
64
  "dependencies": {
64
65
  "@mantine/dates": "^8.3.14",
@@ -68,7 +69,8 @@
68
69
  "@radix-ui/react-radio-group": "^1.3.8",
69
70
  "@radix-ui/react-tabs": "^1.1.13",
70
71
  "clsx": "^2.1.1",
71
- "dayjs": "^1.11.19"
72
+ "dayjs": "^1.11.19",
73
+ "react-daum-postcode": "^3.2.0"
72
74
  },
73
75
  "devDependencies": {
74
76
  "@radix-ui/react-visually-hidden": "^1.2.4",
@@ -5,8 +5,6 @@ import "./index.scss";
5
5
 
6
6
  import { ButtonDefault, ButtonText, ButtonRounded } from "./markup";
7
7
 
8
- export { ButtonDefault, ButtonText, ButtonRounded };
9
-
10
8
  export const Button = {
11
9
  Default: ButtonDefault,
12
10
  Text: ButtonText,
@@ -4,6 +4,10 @@ import clsx from "clsx";
4
4
  import { forwardRef } from "react";
5
5
  import type { ButtonProps } from "../types";
6
6
  import { SlotComponent } from "../../slot";
7
+ import {
8
+ getFormFieldWidthAttr,
9
+ getFormFieldWidthValue,
10
+ } from "../../form/utils/form-field";
7
11
 
8
12
  /**
9
13
  * uds-foundation 토큰 위에서 block/layout/priority/slot API를 제공하는 기본 Button 컴포넌트.
@@ -41,6 +45,7 @@ const ButtonDefault = forwardRef<HTMLElement, ButtonProps>(
41
45
  priority,
42
46
  state: stateProp = "default",
43
47
  block = false,
48
+ width,
44
49
  loading = false,
45
50
  className,
46
51
  type: typeProp,
@@ -65,6 +70,20 @@ const ButtonDefault = forwardRef<HTMLElement, ButtonProps>(
65
70
  "aria-disabled": isDisabled || undefined,
66
71
  };
67
72
 
73
+ const widthAttr =
74
+ width !== undefined
75
+ ? getFormFieldWidthAttr(width)
76
+ : block
77
+ ? "full"
78
+ : undefined;
79
+ const widthValue =
80
+ width !== undefined ? getFormFieldWidthValue(width) : undefined;
81
+ const { style, ...elementRestProps } = restProps;
82
+ const mergedStyle =
83
+ widthValue !== undefined
84
+ ? { ...(style ?? {}), ["--button-width" as const]: widthValue }
85
+ : style;
86
+
68
87
  return (
69
88
  <SlotComponent
70
89
  className={clsx(
@@ -83,8 +102,10 @@ const ButtonDefault = forwardRef<HTMLElement, ButtonProps>(
83
102
  ref={forwardedRef}
84
103
  aria-busy={loading || undefined}
85
104
  data-user-action={userAction}
105
+ data-width={widthAttr}
106
+ style={mergedStyle}
86
107
  {...elementSpecificProps}
87
- {...restProps}
108
+ {...elementRestProps}
88
109
  >
89
110
  {left && (
90
111
  <span className="button-left" data-slot="left">
@@ -150,7 +150,8 @@ $button-priorities: (
150
150
  align-items: center;
151
151
  justify-content: center;
152
152
  gap: var(--button-default-gap-medium, var(--spacing-gap-2, 8px));
153
- width: fit-content;
153
+ width: var(--button-width);
154
+ flex: var(--button-flex);
154
155
  min-width: var(
155
156
  --button-default-width-min-base,
156
157
  var(--theme-size-small-2, 24px)
@@ -216,8 +217,29 @@ $button-priorities: (
216
217
  justify-content: center;
217
218
  }
218
219
 
220
+ &[data-width="auto"] {
221
+ --button-width: auto;
222
+ --button-flex: 0 1 auto;
223
+ }
224
+
225
+ &[data-width="fill"] {
226
+ --button-width: auto;
227
+ --button-flex: 1 1 0%;
228
+ }
229
+
230
+ &[data-width="full"],
219
231
  &.button-block {
220
- width: 100%;
232
+ --button-width: 100%;
233
+ --button-flex: 0 0 100%;
234
+ }
235
+
236
+ &[data-width="fit"] {
237
+ --button-width: fit-content;
238
+ --button-flex: 0 0 auto;
239
+ }
240
+
241
+ &[data-width="custom"] {
242
+ --button-flex: 0 0 auto;
221
243
  }
222
244
 
223
245
  &:not(.button-fill-solid):not(.button-fill-outlined) {
@@ -1,5 +1,9 @@
1
1
  /* 버튼 전용 토큰은 theme root에서 한 번만 정의하며, size 기반 규칙(--button-{type}-{property}-{size})을 따른다. */
2
2
  :root {
3
+ /* layout presets */
4
+ --button-width: fit-content;
5
+ --button-flex: 0 0 auto;
6
+
3
7
  /* default button spacing (size 기반) */
4
8
  --button-default-gap-small: var(--spacing-gap-1);
5
9
  --button-default-gap-medium: var(--spacing-gap-2);
@@ -1,4 +1,5 @@
1
1
  import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
2
+ import type { FormFieldWidth } from "../../form/types";
2
3
 
3
4
  /**
4
5
  * Button; priority 옵션
@@ -91,6 +92,7 @@ type SharedElementProps = Omit<NativeButtonProps, "children"> &
91
92
  * @property {ButtonPriority} priority semantic color priority.
92
93
  * @property {ButtonState} [state] UI 상태. disabled prop과 조합된다.
93
94
  * @property {boolean} [block] width:100% 확장 여부.
95
+ * @property {FormFieldWidth} [width] width preset. block보다 우선 적용된다.
94
96
  * @property {boolean} [loading] true면 readonly 처리 + aria-busy.
95
97
  * @property {ReactNode} [left] 라벨 왼쪽 커스텀 슬롯.
96
98
  * @property {ReactNode} [right] 라벨 오른쪽 커스텀 슬롯.
@@ -145,6 +147,11 @@ export interface ButtonProps extends SharedElementProps {
145
147
  * true면 버튼 폭을 100%로 확장한다.
146
148
  */
147
149
  block?: boolean;
150
+ /**
151
+ * width preset.
152
+ * block보다 우선 적용된다.
153
+ */
154
+ width?: FormFieldWidth;
148
155
  /**
149
156
  * true면 readonly 상태로 전환하고 aria-busy를 설정한다.
150
157
  */
@@ -31,31 +31,32 @@ const getIndicatorIcon = (size: CheckboxSize) =>
31
31
  * @example
32
32
  * <Checkbox size="medium" checked />
33
33
  */
34
- export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
35
- function Checkbox(
36
- { size = "medium", className, disabled, ...restProps },
37
- ref,
38
- ) {
39
- const IndicatorIcon = getIndicatorIcon(size);
34
+ const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(
35
+ { size = "medium", className, disabled, ...restProps },
36
+ ref,
37
+ ) {
38
+ const IndicatorIcon = getIndicatorIcon(size);
40
39
 
41
- return (
42
- <CheckboxPrimitive.Root
43
- ref={ref}
44
- disabled={disabled}
45
- data-size={size}
46
- data-disabled={disabled ? "true" : undefined}
47
- className={clsx(CHECKBOX_CLASSNAME, className)}
48
- {...restProps}
49
- >
50
- <span className={CHECKBOX_SURFACE_CLASSNAME} aria-hidden="true">
51
- <CheckboxPrimitive.Indicator className={CHECKBOX_INDICATOR_CLASSNAME}>
52
- <IndicatorIcon aria-hidden />
53
- </CheckboxPrimitive.Indicator>
54
- </span>
55
- </CheckboxPrimitive.Root>
56
- );
57
- },
58
- );
40
+ return (
41
+ <CheckboxPrimitive.Root
42
+ ref={ref}
43
+ disabled={disabled}
44
+ data-size={size}
45
+ data-disabled={disabled ? "true" : undefined}
46
+ className={clsx(CHECKBOX_CLASSNAME, className)}
47
+ {...restProps}
48
+ >
49
+ <span className={CHECKBOX_SURFACE_CLASSNAME} aria-hidden="true">
50
+ <CheckboxPrimitive.Indicator className={CHECKBOX_INDICATOR_CLASSNAME}>
51
+ <IndicatorIcon aria-hidden />
52
+ </CheckboxPrimitive.Indicator>
53
+ </span>
54
+ </CheckboxPrimitive.Root>
55
+ );
56
+ });
57
+
58
+ // forwardRef Tooltip 유지를 위해 displayName 지정.
59
+ Checkbox.displayName = "Checkbox";
59
60
 
60
61
  /**
61
62
  * 체크박스 필드 컴포넌트; label/helper 텍스트 래퍼
@@ -76,7 +77,7 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
76
77
  * @example
77
78
  * <CheckboxField label="약관 동의" helperText="필수" checked />
78
79
  */
79
- export const CheckboxField = forwardRef<HTMLButtonElement, CheckboxFieldProps>(
80
+ const CheckboxField = forwardRef<HTMLButtonElement, CheckboxFieldProps>(
80
81
  function CheckboxField(
81
82
  {
82
83
  label,
@@ -146,3 +147,8 @@ export const CheckboxField = forwardRef<HTMLButtonElement, CheckboxFieldProps>(
146
147
  );
147
148
  },
148
149
  );
150
+
151
+ // forwardRef Tooltip 유지를 위해 displayName 지정.
152
+ CheckboxField.displayName = "CheckboxField";
153
+
154
+ export { Checkbox, CheckboxField };
@@ -2,7 +2,16 @@ import { DropdownFoundation } from "./foundation";
2
2
  import DropdownTemplate from "./Template";
3
3
 
4
4
  /**
5
- * Dropdown namespace export
5
+ * Dropdown
6
+ * - Provider
7
+ * - Root
8
+ * - Container
9
+ * - Menu
10
+ * - Item
11
+ * - List
12
+ * - Panel
13
+ * - Trigger
14
+ * - Template
6
15
  */
7
16
  export const Dropdown = {
8
17
  ...DropdownFoundation,
@@ -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;