본문으로 건너뛰기
가이드

데이터 모델과 마이그레이션

Note의 분류 필드를 DB, 검증, API, 폼, 목록까지 연결합니다.

데이터 모델과 마이그레이션

Resource 만들기에서 생성한 Note에 선택 입력 항목 분류를 추가합니다. 기존 레코드는 null을 유지하고 새 입력은 80자까지 받습니다. 파일 경로는 모두 App 루트 기준입니다. 배포 업그레이드를 검토하기 전에는 실습 테넌트에만 적용하세요.

컬럼 추가

database/migrations/tenant/2026_09_07_120000_add_category_to_wsp_notes_table.php를 만듭니다. 파일의 시각은 기존 생성 마이그레이션보다 뒤여야 하며 다른 파일과 겹치지 않아야 합니다.

코드 예시
PHP
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('wsp_notes', function (Blueprint $table): void {
            $table->string('category', 80)->nullable();
        });
    }

    public function down(): void
    {
        Schema::table('wsp_notes', function (Blueprint $table): void {
            $table->dropColumn('category');
        });
    }
};

이미 배포한 생성 마이그레이션은 수정하지 않습니다. 위 변경은 기존 행을 유지하지만 down() 실행 시 분류 데이터가 삭제됩니다. 생성된 Manifest는 __DIR__ 기준으로 테넌트 마이그레이션 디렉터리를 반환하므로 Core에 경로를 추가할 필요가 없습니다.

저장과 응답 연결

src/Models/Note.php의 $fillable에 'category'를 넣습니다. nullable 문자열에는 별도 cast가 필요하지 않습니다. src/Http/Controllers/NoteController.php의 store()와 update() 검증 배열 모두에 다음을 추가합니다.

코드 예시
PHP
'category' => ['sometimes', 'nullable', 'string', 'max:80'],

기본 serialize()는 $record->toArray()에서 응답을 만들므로 일반 속성인 category는 목록·상세·저장 응답에 포함됩니다. 직렬화를 바꾼 App이라면 $hidden과 $visible도 확인하세요. 보호 필드에는 이 공개 방식을 적용하지 않습니다.

resources/js/resources/notes/note-resource-contract.ts의 NoteRecord에 category: string | null;을 추가합니다. 요청과 응답에 추가되는 값은 다음과 같습니다. 전체 생성 요청은 아닙니다.

코드 예시
JSON
{ "category": "Planning" }

생성 시에는 기존 name과 단일 legal_entity_public_id도 필요합니다. 수정 요청에서 category를 생략하면 기존 값이 유지되고 null을 보내면 비워집니다.

폼 입력과 정보 표시

resources/js/resources/notes/ 아래 section/NoteFormSection.tsx에서 onSubmit 타입을 { name: string; category: string | null }로 늘립니다. 이름 상태 옆에 아래 코드를 넣고 기존 useMarkTabDirty(name !== initialName) 호출은 교체합니다.

코드 예시
TSX
const initialCategory = initialItem?.category ?? '';
const [category, setCategory] = useState(initialCategory);

useEffect(() => {
    setCategory(initialCategory);
}, [initialCategory]);

useMarkTabDirty(name !== initialName || category !== initialCategory);

제출 코드는 onSubmit({ name, category: category.trim() || null });로 바꾸고 NxResourceInformationForm의 context에 category, setCategory를 추가합니다. surface/NoteFormSurface.tsx의 mutationFn payload 타입도 { name: string; category: string | null }로 맞춥니다. 기존 PUT 전달과 POST의 payload spread가 새 값을 보냅니다. 대상 선택·권한·필드 오류·저장 완료 처리는 유지합니다.

note-information-schema.tsx의 NoteInformationEditContext에 category: string;, setCategory: (category: string) => void;를 추가한 뒤 이름 다음에 다음 필드를 넣습니다.

코드 예시
TSX
{
    key: 'category',
    labelKey: 'workshop.note.category.label',
    view: ({ item }) => resourceInformationValue(item.category),
    edit: {
        kind: 'control',
        error: ({ context }) => firstFieldError(context.fieldErrors, 'category'),
        render: ({ context }, slot) => (
            <NxTextInput
                id={slot.id}
                aria-describedby={slot.describedBy}
                invalid={slot.invalid}
                value={context.category}
                maxLength={80}
                onChange={(event) => {
                    context.setCategory(event.target.value);
                    context.onFieldChange?.('category');
                }}
            />
        ),
    },
},

필요한 컴포넌트는 해당 파일에서 이미 import합니다. 같은 schema의 view가 상세 Section과 Inspector를, edit가 폼을 구성합니다. slot의 속성은 라벨과 오류 안내 연결을 유지하므로 생략하지 않습니다.

목록에 표시

section/NoteListSection.tsx의 컬럼 배열에 다음을 추가합니다. ResourceTextCell은 기존 import를 사용합니다.

코드 예시
TSX
{
    key: 'category',
    header: t('workshop.note.category.label'),
    size: 'meta',
    cell: (row) => (
        <ResourceTextCell primary={row.category ?? t('common.cell.empty')} />
    ),
},

검색·정렬에도 쓰려면 Note::resourceListFields()에 ->field('category', searchable: true, sortable: true)를 추가합니다. 목록 질의 허용 항목은 백엔드에서 관리하므로 프런트엔드에 별도 목록을 만들지 않습니다.

resources/lang/의 각 파일에 workshop.note.category.label을 넣습니다. en.json은 Category, ko.json은 분류, zh.json은 分类입니다. 캐시 갱신과 검사 절차는 App 다국어 처리에 있습니다.

외부 소비자나 Dashboard builder에 공개할지는 별도로 결정합니다. 공개가 필요하면 NoteModule의 descriptor에 nullable string 필드를 선언하고 계약 버전을 검토하세요. DB 컬럼이 있다고 자동으로 공개 descriptor 필드가 되지는 않습니다.

적용 후 확인

Core 루트에서 실습 테넌트 ID를 지정합니다.

코드 예시
Shell
nexia_tenant=REPLACE_WITH_TENANT_ID
task db:tenant:migrate TENANT="$nexia_tenant"

분류를 넣어 생성하고, 수정하고, 비운 뒤 목록·상세·Inspector를 다시 엽니다. 기존 노트도 열리는지 확인하세요. 폼의 maxLength가 일반 입력을 제한하므로 80자 초과 category는 API에 직접 보내 HTTP 422와 필드 오류를 확인합니다. App 테스트하기에서는 API를 통해 저장 결과와 잘못된 입력을 검사합니다.

생성 기준선에는 UUID, 이름, 상태, 시각, soft delete, 소유자 컬럼과 인덱스가 있습니다. 업무별 유일성·감사 증거·동시성 제어까지 자동 제공하지는 않습니다. 필요한 불변 조건은 DB 제약으로 보강하고 충돌하는 쓰기에는 트랜잭션이나 명시적 동시성 검사를 사용합니다. 다른 App의 레코드는 테이블 FK 대신 Resource Reference로 연결합니다.

원본 위치: docs/developers/content/ko/building-apps/data-model-and-migrations.md