fix: 회원 목록에 상태 열을 넣고 가맹점 사용여부의 거짓 표시를 없앤다
가맹점: 조회 SQL이 USE_YN을 select하지 않고 MngrVocVo에도 필드가 없어 값이 오지 않는데 `!== 'N'`으로 읽어 늘 「노출」이었다. 「미노출」로 걸러 낸 행까지 전부 「노출」로 보였다. 값이 없으면 판단하지 않고(null) 목록·모달 모두 고르지 않은 상태로 둔다 — 브랜드명·메모· 등록일과 같은 처리다(사용자 확정). 신규 등록만 「노출」로 시작한다. 회원: 학생·관리자 모두 상태를 바꿀 수는 있는데 목록에 그 열이 없어 확인할 자리가 없었다. 학생은 사용여부(useYn), 관리자는 잠김여부(acctLockYn의 반대)를 열로 넣는다. 값이 없는 행은 `-`다. Co-Authored-By: Claude Opus 5
@84391bdb828eba2ad0edee43786bba7c51afb308
--- app/(protected)/(basic)/admins/_components/admin-member-list.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-list.tsx
... | ... | @@ -10,7 +10,9 @@ |
| 10 | 10 |
type FoxListColumn, |
| 11 | 11 |
} from '@fox/core/components/fox-list-container'; |
| 12 | 12 |
import { FoxSelectText } from '@fox/core/components/fox-select-text';
|
| 13 |
+import { FoxStatusIndicator } from '@fox/core/components/fox-status-indicator';
|
|
| 13 | 14 |
import {
|
| 15 |
+ formatAdminLockStatusLabel, |
|
| 14 | 16 |
formatAdminRoleLabel, |
| 15 | 17 |
formatOptionalValue, |
| 16 | 18 |
type AdminMember, |
... | ... | @@ -98,6 +100,20 @@ |
| 98 | 100 |
), |
| 99 | 101 |
}, |
| 100 | 102 |
{
|
| 103 |
+ key: 'isLocked', |
|
| 104 |
+ header: '잠김여부', |
|
| 105 |
+ width: 120, |
|
| 106 |
+ render: (row) => |
|
| 107 |
+ row.isLocked === null ? ( |
|
| 108 |
+ formatAdminLockStatusLabel(row.isLocked) |
|
| 109 |
+ ) : ( |
|
| 110 |
+ <FoxStatusIndicator |
|
| 111 |
+ type={row.isLocked ? 'danger' : 'success'}
|
|
| 112 |
+ label={formatAdminLockStatusLabel(row.isLocked)}
|
|
| 113 |
+ /> |
|
| 114 |
+ ), |
|
| 115 |
+ }, |
|
| 116 |
+ {
|
|
| 101 | 117 |
key: 'createdAt', |
| 102 | 118 |
header: '생성일', |
| 103 | 119 |
width: 160, |
--- app/(protected)/(basic)/students/_components/student-list.tsx
+++ app/(protected)/(basic)/students/_components/student-list.tsx
... | ... | @@ -9,8 +9,10 @@ |
| 9 | 9 |
type FoxListColumn, |
| 10 | 10 |
} from '@fox/core/components/fox-list-container'; |
| 11 | 11 |
import { FoxSelectText } from '@fox/core/components/fox-select-text';
|
| 12 |
+import { FoxStatusIndicator } from '@fox/core/components/fox-status-indicator';
|
|
| 12 | 13 |
import { formatPhoneNumber } from '@/lib/domain/phone-number';
|
| 13 | 14 |
import {
|
| 15 |
+ formatActiveStatusLabel, |
|
| 14 | 16 |
formatGradeClassNumber, |
| 15 | 17 |
type StudentMember, |
| 16 | 18 |
} from '@/lib/domain/student-member'; |
... | ... | @@ -127,6 +129,20 @@ |
| 127 | 129 |
), |
| 128 | 130 |
}, |
| 129 | 131 |
{
|
| 132 |
+ key: 'isActive', |
|
| 133 |
+ header: '사용여부', |
|
| 134 |
+ width: 120, |
|
| 135 |
+ render: (row) => |
|
| 136 |
+ row.isActive === null ? ( |
|
| 137 |
+ formatActiveStatusLabel(row.isActive) |
|
| 138 |
+ ) : ( |
|
| 139 |
+ <FoxStatusIndicator |
|
| 140 |
+ type={row.isActive ? 'success' : 'danger'}
|
|
| 141 |
+ label={formatActiveStatusLabel(row.isActive)}
|
|
| 142 |
+ /> |
|
| 143 |
+ ), |
|
| 144 |
+ }, |
|
| 145 |
+ {
|
|
| 130 | 146 |
key: 'joinedAt', |
| 131 | 147 |
header: '가입일', |
| 132 | 148 |
width: 160, |
--- app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-form-modal.tsx
... | ... | @@ -60,7 +60,11 @@ |
| 60 | 60 |
); |
| 61 | 61 |
|
| 62 | 62 |
const [categoryCode, setCategoryCode] = useState(merchant?.categoryCode ?? ''); |
| 63 |
- const [isVisible, setIsVisible] = useState(merchant?.isVisible ?? true); |
|
| 63 |
+ // 조회 응답에 useYn이 없어 기존 가맹점은 값을 모른다 — 그때는 어느 쪽도 고르지 않는다. |
|
| 64 |
+ // 신규 등록만 「노출」로 시작한다. |
|
| 65 |
+ const [isVisible, setIsVisible] = useState<boolean | null>( |
|
| 66 |
+ merchant === undefined ? true : merchant.isVisible |
|
| 67 |
+ ); |
|
| 64 | 68 |
// 주소·좌표는 검색 결과가 채운다(기획 ③ Read Only). |
| 65 | 69 |
const [address, setAddress] = useState(merchant?.address ?? ''); |
| 66 | 70 |
const [postalCode, setPostalCode] = useState(merchant?.postalCode ?? ''); |
... | ... | @@ -315,14 +319,14 @@ |
| 315 | 319 |
disabled={isReadOnly}
|
| 316 | 320 |
value="Y" |
| 317 | 321 |
label="노출" |
| 318 |
- checked={isVisible}
|
|
| 322 |
+ checked={isVisible === true}
|
|
| 319 | 323 |
onChange={() => setIsVisible(true)}
|
| 320 | 324 |
/> |
| 321 | 325 |
<FoxRadio |
| 322 | 326 |
disabled={isReadOnly}
|
| 323 | 327 |
value="N" |
| 324 | 328 |
label="미노출" |
| 325 |
- checked={!isVisible}
|
|
| 329 |
+ checked={isVisible === false}
|
|
| 326 | 330 |
onChange={() => setIsVisible(false)}
|
| 327 | 331 |
/> |
| 328 | 332 |
</FoxRadioGroup> |
--- app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
+++ app/(protected)/(basic)/system/merchants/_components/merchant-list.tsx
... | ... | @@ -91,12 +91,15 @@ |
| 91 | 91 |
key: 'isVisible', |
| 92 | 92 |
header: '사용여부', |
| 93 | 93 |
width: 120, |
| 94 |
- render: (row) => ( |
|
| 95 |
- <FoxStatusIndicator |
|
| 96 |
- type={row.isVisible ? 'success' : 'danger'}
|
|
| 97 |
- label={formatVisibilityLabel(row.isVisible)}
|
|
| 98 |
- /> |
|
| 99 |
- ), |
|
| 94 |
+ render: (row) => |
|
| 95 |
+ row.isVisible === null ? ( |
|
| 96 |
+ formatVisibilityLabel(row.isVisible) |
|
| 97 |
+ ) : ( |
|
| 98 |
+ <FoxStatusIndicator |
|
| 99 |
+ type={row.isVisible ? 'success' : 'danger'}
|
|
| 100 |
+ label={formatVisibilityLabel(row.isVisible)}
|
|
| 101 |
+ /> |
|
| 102 |
+ ), |
|
| 100 | 103 |
}, |
| 101 | 104 |
{
|
| 102 | 105 |
key: 'createdAt', |
--- lib/data/repositories/merchant-repository.ts
+++ lib/data/repositories/merchant-repository.ts
... | ... | @@ -81,12 +81,19 @@ |
| 81 | 81 |
postalCode: readString(raw, 'frcsPostZip'), |
| 82 | 82 |
latitude: readString(raw, 'lat'), |
| 83 | 83 |
longitude: readString(raw, 'lng'), |
| 84 |
- // 조회 SQL이 USE_YN을 select하지 않는다(⚠️ 5) — 값이 없으면 노출로 본다. |
|
| 85 |
- isVisible: readString(raw, 'useYn') !== 'N', |
|
| 84 |
+ // 조회 SQL이 USE_YN을 select하지 않는다(⚠️ 5). 값이 없으면 판단하지 않는다 — |
|
| 85 |
+ // 종전처럼 노출로 단정하면 「미노출」로 걸러 낸 행까지 전부 「노출」로 보인다. |
|
| 86 |
+ isVisible: readUseYn(raw), |
|
| 86 | 87 |
}, |
| 87 | 88 |
]; |
| 88 | 89 |
} |
| 89 | 90 |
|
| 91 |
+/** 백엔드 `useYn`은 'Y'/'N' 문자열이다. 응답에 없으면 판단하지 않고 null로 둔다. */ |
|
| 92 |
+function readUseYn(raw: Record<string, unknown>): boolean | null {
|
|
| 93 |
+ const value = raw.useYn; |
|
| 94 |
+ return typeof value === 'string' && value !== '' ? value.toUpperCase() === 'Y' : null; |
|
| 95 |
+} |
|
| 96 |
+ |
|
| 90 | 97 |
export async function fetchMerchants( |
| 91 | 98 |
query: MerchantQuery |
| 92 | 99 |
): Promise<{ items: Merchant[]; totalCount: number }> {
|
--- lib/domain/merchant.ts
+++ lib/domain/merchant.ts
... | ... | @@ -25,7 +25,11 @@ |
| 25 | 25 |
postalCode: string; |
| 26 | 26 |
latitude: string; |
| 27 | 27 |
longitude: string; |
| 28 |
- isVisible: boolean; |
|
| 28 |
+ /** |
|
| 29 |
+ * 조회 응답에 `useYn`이 없다 — 목록 SQL의 SELECT 목록에도 `MngrVocVo`에도 없고 검색 조건 |
|
| 30 |
+ * (`searchUseYn`)에만 쓰인다. 그래서 **모르는 값이 정상**이라 `null`을 둔다(위 넷과 같은 이유). |
|
| 31 |
+ */ |
|
| 32 |
+ isVisible: boolean | null; |
|
| 29 | 33 |
} |
| 30 | 34 |
|
| 31 | 35 |
export const EMPTY_FIELD_PLACEHOLDER = '-'; |
... | ... | @@ -66,7 +70,10 @@ |
| 66 | 70 |
); |
| 67 | 71 |
} |
| 68 | 72 |
|
| 69 |
-export function formatVisibilityLabel(isVisible: boolean): string {
|
|
| 73 |
+export function formatVisibilityLabel(isVisible: boolean | null): string {
|
|
| 74 |
+ if (isVisible === null) {
|
|
| 75 |
+ return EMPTY_FIELD_PLACEHOLDER; |
|
| 76 |
+ } |
|
| 70 | 77 |
return isVisible ? '노출' : '미노출'; |
| 71 | 78 |
} |
| 72 | 79 |
|
--- lib/domain/student-member.ts
+++ lib/domain/student-member.ts
... | ... | @@ -40,7 +40,7 @@ |
| 40 | 40 |
joinedAt: string | null; |
| 41 | 41 |
/** ISO 형식(YYYY-MM-DD) 문자열. */ |
| 42 | 42 |
birthDate: string | null; |
| 43 |
- /** 사용여부. 백엔드가 값도 변경 API도 제공하지 않아 현재는 항상 null이다. */ |
|
| 43 |
+ /** 백엔드 `useYn`. 응답에 없는 행은 null이라 화면에서 `-`로 보인다. */ |
|
| 44 | 44 |
isActive: boolean | null; |
| 45 | 45 |
}; |
| 46 | 46 |
|
... | ... | @@ -54,6 +54,14 @@ |
| 54 | 54 |
return value === null ? EMPTY_FIELD_PLACEHOLDER : String(value); |
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 |
+/** 사용여부 표기. 값이 없으면(백엔드가 `useYn`을 주지 않은 행) `-`다. */ |
|
| 58 |
+export function formatActiveStatusLabel(isActive: boolean | null): string {
|
|
| 59 |
+ if (isActive === null) {
|
|
| 60 |
+ return EMPTY_FIELD_PLACEHOLDER; |
|
| 61 |
+ } |
|
| 62 |
+ return isActive ? '활성' : '비활성'; |
|
| 63 |
+} |
|
| 64 |
+ |
|
| 57 | 65 |
/** |
| 58 | 66 |
* "학년/반/번호" 합성 표기. 표와 조회 팝업이 같은 규칙을 쓰도록 한 곳에 둔다. |
| 59 | 67 |
* |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?