- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동) - 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/) - 기획팀 폴더 requests/ 생성 - plans/ → dev/dev_plans/ 이름 변경 - README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용) - resources.md 신규 (노션 링크용, assets/brochure 이관 예정) - CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동 - 전체 참조 경로 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
tenant_id 준수 분석 및 분리 방안
작성일: 2026-01-29 목적: API 전체 모델에서 tenant_id 스코핑 미적용 현황을 분석하고, BelongsToTenant trait 적용 방안 수립 기준 문서:
docs/specs/database-schema.md,docs/architecture/system-overview.md상태: 🔄 분석 완료 → 실행 대기
📍 현재 진행 상태
| 항목 | 내용 |
|---|---|
| 마지막 완료 작업 | 전체 모델 분석 완료 |
| 다음 작업 | 사용자 검토 후 Phase 1 실행 |
| 진행률 | 0/4 Phase (0%) |
| 마지막 업데이트 | 2026-01-29 |
1. 개요
1.1 배경
SAM API는 멀티테넌트 아키텍처를 사용하며, BelongsToTenant trait를 통해 자동 tenant_id 스코핑을 적용합니다. 그러나 일부 모델에서 trait가 누락되어 있어, 테넌트 간 데이터 격리가 보장되지 않을 수 있습니다.
분석 결과 요약:
- 전체 모델: 167개
- BelongsToTenant 적용: 103개 (61.7%)
- 미적용: 63개 (37.7%)
- 의도적 글로벌: 18개
- 부모 종속 (FK 격리): 13개
- BelongsToTenant 추가 필요: 27개
- 검토 후 결정: 5개
1.2 기준 원칙
┌─────────────────────────────────────────────────────────────────┐
│ 🎯 핵심 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ 1. tenant_id 컬럼이 있는 모델은 BelongsToTenant 적용 필수 │
│ 2. 부모-자식 관계에서 자식은 부모의 FK로 격리 가능하면 면제 │
│ 3. 시스템 전역 데이터(User, Tenant, ApiKey 등)는 글로벌 유지 │
│ 4. Boards 영역은 시스템/테넌트 혼용이므로 커스텀 스코프 유지 │
└─────────────────────────────────────────────────────────────────┘
1.3 변경 승인 정책
| 분류 | 예시 | 승인 |
|---|---|---|
| ✅ 즉시 가능 | BelongsToTenant trait 추가 (기존 동작 유지) | 불필요 |
| ⚠️ 컨펌 필요 | Boards 영역 스코핑 방식 변경, 쿼리 로직 수정 | 필수 |
| 🔴 금지 | 테이블 구조 변경, tenant_id 컬럼 추가/삭제 | 별도 협의 |
1.4 준수 규칙
docs/specs/database-schema.md- 테이블 구조docs/architecture/system-overview.md- 시스템 아키텍처docs/standards/quality-checklist.md- 품질 체크리스트
2. 분석 결과 상세
2.1 의도적 글로벌 (BelongsToTenant 불필요) - 18개
시스템 전역 데이터로, tenant_id 스코핑이 필요하지 않습니다.
| # | 모델 | 테이블 | tenant_id | 사유 |
|---|---|---|---|---|
| 1 | ApiKey |
api_keys | ❌ | 시스템 API 키 |
| 2 | ApiRequestLog |
api_request_logs | ❌ | 시스템 감시 로그 |
| 3 | AuditLog |
audit_logs | ✅ | 전체 감사 로그 (자체 tenant_id 필터링) |
| 4 | FcmSendLog |
fcm_send_logs | ✅ | 푸시 발송 로그 (수동 필터링) |
| 5 | LoginToken |
login_tokens | ❌ | MNG→API 인증 토큰 |
| 6 | SiteAdmin |
site_admins | ❌ | 시스템 관리자 |
| 7 | Tenant |
tenants | ❌ | 테넌트 마스터 (자기 자신) |
| 8 | Plan |
plans | ❌ | 구독 플랜 정의 |
| 9 | Subscription |
subscriptions | ✅ | 구독 (tenant_id로 연결) |
| 10 | Payment |
payments | ✅ | 결제 (tenant_id로 연결) |
| 11 | User |
users | ❌ | 글로벌 사용자 계정 |
| 12 | UserTenant |
user_tenants | ✅ | 사용자-테넌트 매핑 (피벗) |
| 13 | UserRole |
user_roles | ✅ | 사용자-역할 매핑 (피벗) |
| 14 | GlobalMenu |
global_menus | ❌ | 시스템 전역 메뉴 |
| 15 | Tag |
tags | ❌ | 시스템 공용 태그 |
| 16 | SystemFieldDefinition |
system_field_definitions | ❌ | 시스템 필드 정의 |
| 17 | KdPriceTable |
kd_price_tables | ❌ | 경동 전용 단가표 (레거시) |
| 18 | TenantScope |
- | - | 스코프 정의 클래스 (모델 아님) |
2.2 부모 종속 (FK 격리 - BelongsToTenant 면제) - 13개
부모 모델에 BelongsToTenant가 적용되어 있고, FK를 통해 자동 격리되는 자식 모델입니다.
| # | 모델 | 테이블 | 부모 모델 | FK |
|---|---|---|---|---|
| 1 | PostCustomFieldValue |
post_custom_field_values | Post | post_id |
| 2 | DocumentData |
document_data | Document | document_id |
| 3 | DocumentApproval |
document_approvals | Document | document_id |
| 4 | DocumentAttachment |
document_attachments | Document | document_id |
| 5 | RoleMenuPermission |
role_menu_permissions | Role | role_id |
| 6 | UserMenuPermission |
user_menu_permissions | - | user_id + menu_id |
| 7 | NotificationSettingGroupItem |
notification_setting_group_items | NotificationSettingGroup | group_id |
| 8 | BillInstallment |
bill_installments | Bill | bill_id |
| 9 | ApprovalStep |
approval_steps | Approval | approval_id |
| 10 | OrderItemComponent |
order_item_components | OrderItem | order_item_id |
| 11 | MaterialInspectionItem |
inspection_items | MaterialInspection | inspection_id |
| 12 | QuoteFormulaItem |
quote_formula_items | QuoteFormula | formula_id |
| 13 | QuoteFormulaRange |
quote_formula_ranges | QuoteFormula | formula_id |
주의: 이 모델들은 직접 쿼리할 때 부모를 통한 접근이 필수입니다. 직접 ::all() 등으로 접근하면 테넌트 격리가 안됩니다.
2.3 🔴 BelongsToTenant 추가 필요 - 27개
tenant_id 컬럼이 있지만 BelongsToTenant trait가 없는 모델입니다. 추가해야 합니다.
| # | 모델 | 테이블 | 영역 | 우선순위 |
|---|---|---|---|---|
| Design 영역 (4개) | ||||
| 1 | DesignModel |
models | 설계 | 높음 |
| 2 | ModelVersion |
model_versions | 설계 | 높음 |
| 3 | BomTemplate |
bom_templates | 설계 | 높음 |
| 4 | BomTemplateItem |
bom_template_items | 설계 | 높음 |
| Orders 영역 (2개) | ||||
| 5 | OrderHistory |
order_histories | 수주 | 높음 |
| 6 | OrderVersion |
order_versions | 수주 | 높음 |
| Materials 영역 (3개) | ||||
| 7 | MaterialReceipt |
material_receipts | 자재 | 높음 |
| 8 | MaterialInspection |
inspections | 자재 | 높음 |
| 9 | MaterialInspectionItem |
inspection_items | 자재 | 중간 |
| Tenants 설정 영역 (7개) | ||||
| 10 | TenantOptionGroup |
tenant_option_groups | 설정 | 높음 |
| 11 | TenantOptionValue |
tenant_option_values | 설정 | 높음 |
| 12 | TenantFieldSetting |
tenant_field_settings | 설정 | 높음 |
| 13 | TenantUserProfile |
tenant_user_profiles | 설정 | 중간 |
| 14 | SettingFieldDef |
setting_field_defs | 설정 | 중간 |
| 15 | TenantStatField |
tenant_stat_fields | 설정 | 중간 |
| 16 | BarobillSetting |
barobill_settings | 설정 | 낮음 |
| BadDebts 영역 (2개) | ||||
| 17 | BadDebtDocument |
bad_debt_documents | 미수금 | 중간 |
| 18 | BadDebtMemo |
bad_debt_memos | 미수금 | 중간 |
| Permissions 영역 (2개) | ||||
| 19 | Permission |
permissions | 권한 | 높음 |
| 20 | PermissionOverride |
permission_overrides | 권한 | 높음 |
| 기타 영역 (8개) | ||||
| 21 | MainRequest |
main_requests | 요청 | 높음 |
| 22 | MainRequestFlow |
main_request_flows | 요청 | 높음 |
| 23 | MainRequestEstimate |
main_request_estimates | 견적 | 높음 |
| 24 | Schedule |
schedules | 일정 | 중간 |
| 25 | ProcessItem |
process_items | 공정 | 중간 |
| 26 | ProcessClassificationRule |
process_classification_rules | 공정 | 중간 |
| 27 | ItemDetail |
item_details | 품목 | 중간 |
2.4 ⚠️ 검토 후 결정 필요 - 5개
커스텀 스코핑을 사용하거나, 특수한 비즈니스 로직이 있는 모델입니다.
| # | 모델 | 현재 방식 | 검토 사항 |
|---|---|---|---|
| 1 | Board |
scopeAccessible() 커스텀 |
tenant_id nullable; 시스템 게시판(tenant_id=null)과 테넌트 게시판 혼용 |
| 2 | Post |
수동 where 조건 | Board의 tenant_id 정책을 따름 |
| 3 | BoardComment |
수동 where 조건 | Post 종속 |
| 4 | BoardSetting |
수동 where 조건 | Board 종속 |
| 5 | CompanyRequest |
created_tenant_id |
tenant_id 대신 created_tenant_id 사용 |
Board 영역 결론: 시스템 게시판(tenant_id=null)을 지원해야 하므로, BelongsToTenant의 글로벌 스코프 대신 현재 커스텀 스코프(scopeAccessible) 유지가 적합합니다.
2.5 특수 케이스
| 모델 | 설명 |
|---|---|
Part |
tenant_id 있지만 BelongsToTenant 미적용. Product와 유사 역할이나 별도 테이블 |
Lot / LotSale |
tenant_id 있지만 미적용. 품질관리 영역 |
CalculationConfig |
tenant_id 있지만 미적용. 계산 설정 |
QuoteFormulaMapping |
tenant_id 있지만 미적용. 견적 수식 매핑 |
3. 작업 절차
3.1 단계별 절차
Phase 1: 고우선순위 모델 적용 (15개)
├── Design 영역: DesignModel, ModelVersion, BomTemplate, BomTemplateItem
├── Orders 영역: OrderHistory, OrderVersion
├── Materials 영역: MaterialReceipt, MaterialInspection
├── Permissions 영역: Permission, PermissionOverride
├── MainRequest 영역: MainRequest, MainRequestFlow, MainRequestEstimate
├── Tenants 설정: TenantOptionGroup, TenantOptionValue, TenantFieldSetting
└── 검증: 기존 서비스 로직에 중복 where 조건 확인 및 정리
Phase 2: 중간 우선순위 모델 적용 (10개)
├── Tenants 설정: TenantUserProfile, SettingFieldDef, TenantStatField
├── BadDebts: BadDebtDocument, BadDebtMemo
├── 기타: Schedule, ProcessItem, ProcessClassificationRule, ItemDetail
├── Materials: MaterialInspectionItem
└── 검증: 서비스 로직 정리
Phase 3: 특수 케이스 처리 (4개)
├── Part, Lot, LotSale, CalculationConfig, QuoteFormulaMapping
└── 각 모델별 비즈니스 로직 확인 후 적용
Phase 4: 서비스 레이어 정리
├── BelongsToTenant 적용 후 불필요한 수동 where('tenant_id', ...) 제거
├── 직접 쿼리하는 부모 종속 모델에 대한 접근 패턴 검토
└── 전체 통합 테스트
4. 상세 작업 내용
4.1 Phase 1: 고우선순위 (15개)
각 모델에 use BelongsToTenant; 추가. 작업 패턴:
// Before
namespace App\Models\Design;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
class DesignModel extends Model
{
use ModelTrait;
// ...
}
// After
namespace App\Models\Design;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
class DesignModel extends Model
{
use BelongsToTenant, ModelTrait;
// ...
}
Phase 1 대상 모델:
| # | 파일 경로 | 상태 |
|---|---|---|
| 1.1 | app/Models/Design/DesignModel.php |
⏳ |
| 1.2 | app/Models/Design/ModelVersion.php |
⏳ |
| 1.3 | app/Models/Design/BomTemplate.php |
⏳ |
| 1.4 | app/Models/Design/BomTemplateItem.php |
⏳ |
| 1.5 | app/Models/Orders/OrderHistory.php |
⏳ |
| 1.6 | app/Models/Orders/OrderVersion.php |
⏳ |
| 1.7 | app/Models/Materials/MaterialReceipt.php |
⏳ |
| 1.8 | app/Models/Materials/MaterialInspection.php |
⏳ |
| 1.9 | app/Models/Permissions/Permission.php |
⏳ |
| 1.10 | app/Models/Permissions/PermissionOverride.php |
⏳ |
| 1.11 | app/Models/MainRequest.php |
⏳ |
| 1.12 | app/Models/MainRequestFlow.php |
⏳ |
| 1.13 | app/Models/Estimates/MainRequestEstimate.php |
⏳ |
| 1.14 | app/Models/Tenants/TenantOptionGroup.php |
⏳ |
| 1.15 | app/Models/Tenants/TenantOptionValue.php |
⏳ |
| 1.16 | app/Models/Tenants/TenantFieldSetting.php |
⏳ |
4.2 Phase 2: 중간 우선순위 (10개)
| # | 파일 경로 | 상태 |
|---|---|---|
| 2.1 | app/Models/Tenants/TenantUserProfile.php |
⏳ |
| 2.2 | app/Models/Tenants/SettingFieldDef.php |
⏳ |
| 2.3 | app/Models/Tenants/TenantStatField.php |
⏳ |
| 2.4 | app/Models/BadDebts/BadDebtDocument.php |
⏳ |
| 2.5 | app/Models/BadDebts/BadDebtMemo.php |
⏳ |
| 2.6 | app/Models/Tenants/Schedule.php |
⏳ |
| 2.7 | app/Models/ProcessItem.php |
⏳ |
| 2.8 | app/Models/ProcessClassificationRule.php |
⏳ |
| 2.9 | app/Models/Items/ItemDetail.php |
⏳ |
| 2.10 | app/Models/Materials/MaterialInspectionItem.php |
⏳ |
4.3 Phase 3: 특수 케이스 (5개)
| # | 파일 경로 | 검토 사항 | 상태 |
|---|---|---|---|
| 3.1 | app/Models/Products/Part.php |
Product와 역할 중복 여부 | ⏳ |
| 3.2 | app/Models/Qualitys/Lot.php |
품질관리 독립 쿼리 패턴 | ⏳ |
| 3.3 | app/Models/Qualitys/LotSale.php |
Lot 종속 여부 | ⏳ |
| 3.4 | app/Models/Calculation/CalculationConfig.php |
테넌트별 설정 확인 | ⏳ |
| 3.5 | app/Models/Quote/QuoteFormulaMapping.php |
공용 vs 테넌트별 | ⏳ |
4.4 Phase 4: 서비스 레이어 정리
BelongsToTenant 적용 후, 서비스에서 수동으로 ->where('tenant_id', $tenantId) 하던 코드를 정리합니다.
확인 대상 서비스:
app/Services/Design/- DesignModelService, BomTemplateService 등app/Services/OrderService.php- OrderHistory, OrderVersion 관련app/Services/Materials/- MaterialReceiptService 등app/Services/MainRequestService.php- 기타 관련 서비스
정리 패턴:
// Before (수동 스코핑)
$models = DesignModel::where('tenant_id', $this->tenantId())->get();
// After (BelongsToTenant 자동 스코핑)
$models = DesignModel::all(); // 글로벌 스코프가 자동 적용
5. 컨펌 대기 목록
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|---|---|---|---|
| 1 | Boards 영역 | 현재 커스텀 스코프 유지 vs BelongsToTenant 전환 | Board, Post, BoardComment, BoardSetting | ⚠️ 확인 필요 |
| 2 | Permission 영역 | Spatie Permission 패키지와의 호환성 | Permission, PermissionOverride | ⚠️ 확인 필요 |
| 3 | Schedule 모델 | tenant_id nullable - 글로벌 일정 지원 유지 여부 | Schedule | ⚠️ 확인 필요 |
| 4 | CompanyRequest | created_tenant_id → tenant_id 통일 여부 | CompanyRequest | ⚠️ 확인 필요 |
6. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|---|---|---|---|---|
| 2026-01-29 | 분석 | 전체 모델 tenant_id 준수 분석 완료 | 167개 모델 분석 | - |
| 2026-01-29 | 문서 | 계획 문서 초안 작성 | docs/dev_plans/tenant-id-compliance-plan.md | - |
7. 참고 문서
- DB 스키마:
docs/specs/database-schema.md - 시스템 아키텍처:
docs/architecture/system-overview.md - API 규칙:
docs/standards/api-rules.md - 품질 체크리스트:
docs/standards/quality-checklist.md - BelongsToTenant trait:
api/app/Traits/BelongsToTenant.php - TenantScope:
api/app/Models/Scopes/TenantScope.php
8. 리스크 및 주의사항
8.1 BelongsToTenant 적용 시 부작용
| 리스크 | 설명 | 대응 |
|---|---|---|
| 중복 스코핑 | 서비스에서 이미 where('tenant_id', ...) 하고 있으면 중복 |
Phase 4에서 수동 where 제거 |
| withoutGlobalScope 필요 | 관리자 기능에서 전체 데이터 조회 시 | withoutGlobalScopes() 명시 |
| 테스트 실패 | tenant_id 컨텍스트 없는 테스트 | 테스트에서 tenant context 설정 |
| 마이그레이션 영향 | seeder/migration에서 직접 insert | tenant context 없이 실행 가능한지 확인 |
8.2 Permission 모델 특수 사항
Permission은 Spatie Permission 패키지를 확장하므로, BelongsToTenant 적용 시 패키지 내부 쿼리와 충돌 가능성이 있습니다. 적용 전 테스트가 필수입니다.
8.3 Schedule 모델 특수 사항
Schedule의 tenant_id는 nullable입니다. BelongsToTenant 적용 시 tenant_id=null인 글로벌 일정이 조회되지 않을 수 있습니다. TenantScope에서 nullable 처리가 필요할 수 있습니다.
9. 검증 결과
작업 완료 후 이 섹션에 검증 결과 추가
9.1 성공 기준
| 기준 | 측정 방법 | 달성 |
|---|---|---|
| Phase 1 모델 전체 BelongsToTenant 적용 | 코드 확인 | ⏳ |
| Phase 2 모델 전체 BelongsToTenant 적용 | 코드 확인 | ⏳ |
| 서비스 레이어 중복 where 제거 | grep 검색 | ⏳ |
| 기존 API 기능 정상 동작 | Swagger 테스트 | ⏳ |
| Pint 포맷팅 통과 | ./vendor/bin/pint --test |
⏳ |
10. 자기완결성 점검 결과
10.1 체크리스트 검증
| # | 검증 항목 | 상태 | 비고 |
|---|---|---|---|
| 1 | 작업 목적이 명확한가? | ✅ | tenant_id 미적용 모델에 BelongsToTenant 추가 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.1 참조 |
| 3 | 작업 범위가 구체적인가? | ✅ | 27개 모델 명시, 5개 검토 대상 분리 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Spatie Permission, Boards 커스텀 스코프 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 전체 모델 파일 경로 명시 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | Phase 1~4 구체적 작업 항목 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 성공 기준 및 테스트 방법 |
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 모델명, 파일 경로, 수치 사용 |
10.2 새 세션 시뮬레이션 테스트
| 질문 | 답변 가능 | 참조 섹션 |
|---|---|---|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 4.1 Phase 1 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2.3 수정 필요 목록 + 4.1~4.3 파일 경로 |
| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 성공 기준 |
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
결과: 5/5 통과 → ✅ 자기완결성 확보
이 문서는 /sc:plan 스킬로 생성되었습니다.