diff --git a/TaxBaik.Application/DependencyInjection.cs b/TaxBaik.Application/DependencyInjection.cs index 5bc5e9a..ef420d6 100644 --- a/TaxBaik.Application/DependencyInjection.cs +++ b/TaxBaik.Application/DependencyInjection.cs @@ -19,6 +19,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/TaxBaik.Application/Services/CompanyService.cs b/TaxBaik.Application/Services/CompanyService.cs new file mode 100644 index 0000000..5ff4436 --- /dev/null +++ b/TaxBaik.Application/Services/CompanyService.cs @@ -0,0 +1,95 @@ +namespace TaxBaik.Application.Services; + +using TaxBaik.Domain.Entities; +using TaxBaik.Domain.Interfaces; + +public class CompanyService(ICompanyRepository repository) +{ + public async Task CreateAsync(string companyCode, string companyName, string? contactPerson = null, + string? phone = null, string? email = null, string? memo = null, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(companyCode)) + throw new ValidationException("회사 코드를 입력하세요."); + + if (string.IsNullOrWhiteSpace(companyName)) + throw new ValidationException("회사명을 입력하세요."); + + var existing = await repository.GetByCodeAsync(companyCode.Trim(), ct); + if (existing != null) + throw new ValidationException("이미 존재하는 회사 코드입니다."); + + var company = new Company + { + CompanyCode = companyCode.Trim(), + CompanyName = companyName.Trim(), + ContactPerson = string.IsNullOrWhiteSpace(contactPerson) ? null : contactPerson.Trim(), + Phone = string.IsNullOrWhiteSpace(phone) ? null : phone.Trim(), + Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim(), + Memo = string.IsNullOrWhiteSpace(memo) ? null : memo.Trim(), + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + return await repository.CreateAsync(company, ct); + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) => + await repository.GetByIdAsync(id, ct); + + public async Task GetByCodeAsync(string code, CancellationToken ct = default) => + await repository.GetByCodeAsync(code, ct); + + public async Task> GetAllActiveAsync(CancellationToken ct = default) => + await repository.GetAllActiveAsync(ct); + + public async Task<(IEnumerable, int)> GetPagedAsync(int page, int pageSize, CancellationToken ct = default) + { + var (items, total) = await repository.GetPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), ct); + return (items, total); + } + + public async Task UpdateAsync(int id, string companyCode, string companyName, string? contactPerson = null, + string? phone = null, string? email = null, string? memo = null, bool isActive = true, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(companyCode)) + throw new ValidationException("회사 코드를 입력하세요."); + + if (string.IsNullOrWhiteSpace(companyName)) + throw new ValidationException("회사명을 입력하세요."); + + var company = await repository.GetByIdAsync(id, ct); + if (company == null) + throw new ValidationException("회사를 찾을 수 없습니다."); + + var existing = await repository.GetByCodeAsync(companyCode.Trim(), ct); + if (existing != null && existing.Id != id) + throw new ValidationException("이미 존재하는 회사 코드입니다."); + + company.CompanyCode = companyCode.Trim(); + company.CompanyName = companyName.Trim(); + company.ContactPerson = string.IsNullOrWhiteSpace(contactPerson) ? null : contactPerson.Trim(); + company.Phone = string.IsNullOrWhiteSpace(phone) ? null : phone.Trim(); + company.Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim(); + company.Memo = string.IsNullOrWhiteSpace(memo) ? null : memo.Trim(); + company.IsActive = isActive; + company.UpdatedAt = DateTime.UtcNow; + + await repository.UpdateAsync(company, ct); + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + var company = await repository.GetByIdAsync(id, ct); + if (company == null) + throw new ValidationException("회사를 찾을 수 없습니다."); + + if (company.CompanyCode == "DEFAULT") + throw new ValidationException("기본 회사는 삭제할 수 없습니다."); + + await repository.DeleteAsync(id, ct); + } + + private static int NormalizePage(int page) => Math.Max(1, page); + private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 1, 100); +} diff --git a/TaxBaik.Domain/Entities/Company.cs b/TaxBaik.Domain/Entities/Company.cs new file mode 100644 index 0000000..80a2db6 --- /dev/null +++ b/TaxBaik.Domain/Entities/Company.cs @@ -0,0 +1,15 @@ +namespace TaxBaik.Domain.Entities; + +public class Company +{ + public int Id { get; set; } + public string CompanyCode { get; set; } = ""; + public string CompanyName { get; set; } = ""; + public string? ContactPerson { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public string? Memo { get; set; } + public bool IsActive { get; set; } = true; + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/TaxBaik.Domain/Interfaces/ICompanyRepository.cs b/TaxBaik.Domain/Interfaces/ICompanyRepository.cs new file mode 100644 index 0000000..67bef5e --- /dev/null +++ b/TaxBaik.Domain/Interfaces/ICompanyRepository.cs @@ -0,0 +1,14 @@ +namespace TaxBaik.Domain.Interfaces; + +using TaxBaik.Domain.Entities; + +public interface ICompanyRepository +{ + Task CreateAsync(Company company, CancellationToken cancellationToken = default); + Task GetByIdAsync(int id, CancellationToken cancellationToken = default); + Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); + Task> GetAllActiveAsync(CancellationToken cancellationToken = default); + Task<(IEnumerable Items, int Total)> GetPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default); + Task UpdateAsync(Company company, CancellationToken cancellationToken = default); + Task DeleteAsync(int id, CancellationToken cancellationToken = default); +} diff --git a/TaxBaik.Infrastructure/DependencyInjection.cs b/TaxBaik.Infrastructure/DependencyInjection.cs index 0b112e8..9c99c09 100644 --- a/TaxBaik.Infrastructure/DependencyInjection.cs +++ b/TaxBaik.Infrastructure/DependencyInjection.cs @@ -20,6 +20,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/TaxBaik.Infrastructure/Repositories/CompanyRepository.cs b/TaxBaik.Infrastructure/Repositories/CompanyRepository.cs new file mode 100644 index 0000000..90e8908 --- /dev/null +++ b/TaxBaik.Infrastructure/Repositories/CompanyRepository.cs @@ -0,0 +1,82 @@ +namespace TaxBaik.Infrastructure.Repositories; + +using Dapper; +using TaxBaik.Domain.Entities; +using TaxBaik.Domain.Interfaces; + +public class CompanyRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ICompanyRepository +{ + public async Task CreateAsync(Company company, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + return await conn.QueryFirstAsync( + @"INSERT INTO companies (company_code, company_name, contact_person, phone, email, memo, is_active, created_at, updated_at) + VALUES (@CompanyCode, @CompanyName, @ContactPerson, @Phone, @Email, @Memo, @IsActive, NOW(), NOW()) + RETURNING id", + company); + } + + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + return await conn.QueryFirstOrDefaultAsync( + @"SELECT id, company_code, company_name, contact_person, phone, email, memo, is_active, created_at, updated_at + FROM companies WHERE id = @Id", + new { Id = id }); + } + + public async Task GetByCodeAsync(string code, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + return await conn.QueryFirstOrDefaultAsync( + @"SELECT id, company_code, company_name, contact_person, phone, email, memo, is_active, created_at, updated_at + FROM companies WHERE company_code = @Code", + new { Code = code }); + } + + public async Task> GetAllActiveAsync(CancellationToken cancellationToken = default) + { + using var conn = Conn(); + return await conn.QueryAsync( + @"SELECT id, company_code, company_name, contact_person, phone, email, memo, is_active, created_at, updated_at + FROM companies WHERE is_active = TRUE ORDER BY company_name"); + } + + public async Task<(IEnumerable Items, int Total)> GetPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + var offset = (page - 1) * pageSize; + + using var reader = await conn.QueryMultipleAsync( + @"SELECT id, company_code, company_name, contact_person, phone, email, memo, is_active, created_at, updated_at + FROM companies + ORDER BY company_name + LIMIT @PageSize OFFSET @Offset; + + SELECT COUNT(*) FROM companies;", + new { PageSize = pageSize, Offset = offset }); + + var items = (await reader.ReadAsync()).ToList(); + var total = await reader.ReadFirstAsync(); + + return (items, total); + } + + public async Task UpdateAsync(Company company, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + await conn.ExecuteAsync( + @"UPDATE companies + SET company_code = @CompanyCode, company_name = @CompanyName, + contact_person = @ContactPerson, phone = @Phone, email = @Email, + memo = @Memo, is_active = @IsActive, updated_at = NOW() + WHERE id = @Id", + company); + } + + public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + await conn.ExecuteAsync("DELETE FROM companies WHERE id = @Id", new { Id = id }); + } +} diff --git a/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor b/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor new file mode 100644 index 0000000..54d653d --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Forms/CompanyForm.razor @@ -0,0 +1,88 @@ +@using TaxBaik.Application.Services + + + + + + + + + + + + + + + + +
+ + @ButtonText + + 취소 +
+
+ +@code { + [Parameter, EditorRequired] + public string ButtonText { get; set; } = "저장"; + + [Parameter] + public EventCallback OnSubmit { get; set; } + + [Parameter] + public EventCallback OnCancel { get; set; } + + [Parameter] + public CompanyFormModel? InitialData { get; set; } + + private MudForm? form; + private CompanyFormModel model = new(); + + protected override void OnInitialized() + { + if (InitialData != null) + { + model = new CompanyFormModel + { + CompanyCode = InitialData.CompanyCode, + CompanyName = InitialData.CompanyName, + ContactPerson = InitialData.ContactPerson, + Phone = InitialData.Phone, + Email = InitialData.Email, + Memo = InitialData.Memo, + IsActive = InitialData.IsActive + }; + } + } + + private async Task HandleSubmit() + { + if (form == null) + return; + + await form.Validate(); + if (!form.IsValid) + return; + + await OnSubmit.InvokeAsync(model); + } + + public class CompanyFormModel + { + public string CompanyCode { get; set; } = ""; + public string CompanyName { get; set; } = ""; + public string? ContactPerson { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public string? Memo { get; set; } + public bool IsActive { get; set; } = true; + } +} diff --git a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor new file mode 100644 index 0000000..2534826 --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyCreate.razor @@ -0,0 +1,51 @@ +@page "/admin/companies/create" +@attribute [Authorize] +@using TaxBaik.Web.Components.Admin.Forms +@inject IApiClient ApiClient +@inject NavigationManager Navigation +@inject ISnackbar Snackbar + +고객사 등록 + +
+
+ Settings + 새 고객사 등록 + 새로운 고객사를 추가합니다. +
+ 취소 +
+ + + + + +@code { + private void GoBack() + { + Navigation.NavigateTo("/taxbaik/admin/companies"); + } + + private async Task HandleCreate(CompanyForm.CompanyFormModel model) + { + try + { + await ApiClient.PostAsync("company", new + { + companyCode = model.CompanyCode, + companyName = model.CompanyName, + contactPerson = model.ContactPerson, + phone = model.Phone, + email = model.Email, + memo = model.Memo + }); + + Snackbar.Add("고객사가 등록되었습니다.", Severity.Success); + Navigation.NavigateTo("/taxbaik/admin/companies"); + } + catch (Exception ex) + { + Snackbar.Add($"등록 실패: {ex.Message}", Severity.Error); + } + } +} diff --git a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor new file mode 100644 index 0000000..deb247e --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyEdit.razor @@ -0,0 +1,128 @@ +@page "/admin/companies/{id:int}/edit" +@attribute [Authorize] +@using TaxBaik.Web.Components.Admin.Forms +@inject IApiClient ApiClient +@inject NavigationManager Navigation +@inject ISnackbar Snackbar +@inject IDialogService DialogService + +고객사 수정 + +
+
+ Settings + 고객사 수정 + 고객사 정보를 수정합니다. +
+ 취소 +
+ +@if (isLoading) +{ + +} +else if (formModel == null) +{ + 고객사를 찾을 수 없습니다. +} +else +{ + + + + + + + 고객사 삭제 + + +} + +@code { + [Parameter] + public int Id { get; set; } + + private CompanyForm.CompanyFormModel? formModel; + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + try + { + var company = await ApiClient.GetAsync($"company/{Id}"); + IDictionary? dict = company as IDictionary; + if (dict != null) + { + formModel = new CompanyForm.CompanyFormModel + { + CompanyCode = (string)dict["companyCode"], + CompanyName = (string)dict["companyName"], + ContactPerson = (string?)dict["contactPerson"], + Phone = (string?)dict["phone"], + Email = (string?)dict["email"], + Memo = (string?)dict["memo"], + IsActive = (bool)(dynamic)dict["isActive"] + }; + } + } + catch (Exception ex) + { + Snackbar.Add($"고객사 로드 실패: {ex.Message}", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private void GoBack() + { + Navigation.NavigateTo("/taxbaik/admin/companies"); + } + + private async Task HandleUpdate(CompanyForm.CompanyFormModel model) + { + try + { + await ApiClient.PutAsync($"company/{Id}", new + { + companyCode = model.CompanyCode, + companyName = model.CompanyName, + contactPerson = model.ContactPerson, + phone = model.Phone, + email = model.Email, + memo = model.Memo, + isActive = model.IsActive + }); + + Snackbar.Add("고객사가 수정되었습니다.", Severity.Success); + Navigation.NavigateTo("/taxbaik/admin/companies"); + } + catch (Exception ex) + { + Snackbar.Add($"수정 실패: {ex.Message}", Severity.Error); + } + } + + private async Task DeleteCompany() + { + var result = await DialogService.ShowMessageBox( + "고객사 삭제", + "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "삭제", "취소"); + + if (result != true) + return; + + try + { + await ApiClient.DeleteAsync($"company/{Id}"); + Snackbar.Add("고객사가 삭제되었습니다.", Severity.Success); + Navigation.NavigateTo("/taxbaik/admin/companies"); + } + catch (Exception ex) + { + Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); + } + } +} diff --git a/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor new file mode 100644 index 0000000..b0cd304 --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Pages/Companies/CompanyList.razor @@ -0,0 +1,134 @@ +@page "/admin/companies" +@attribute [Authorize] +@inject IApiClient ApiClient +@inject ISnackbar Snackbar + +고객사 관리 + +
+
+ Settings + 고객사 관리 + 등록된 고객사를 관리하고 새로운 고객사를 추가합니다. +
+ 새 고객사 등록 +
+ + + + @($"전체 고객사 {totalCompanies}개") + 페이지 @currentPage / @totalPages + + + + + + + + + + + + + + + + + + + 수정 + + + + + + + 이전 + 다음 + + +@code { + private List companies = []; + private bool isLoading = true; + private int currentPage = 1; + private int totalPages = 1; + private int totalCompanies = 0; + private const int PageSize = 20; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + try + { + isLoading = true; + var response = await ApiClient.GetAsync($"company?page={currentPage}&pageSize={PageSize}"); + + IDictionary? dict = response as IDictionary; + if (dict != null) + { + totalCompanies = (int)(dynamic)dict["total"]; + totalPages = (totalCompanies + PageSize - 1) / PageSize; + + if (dict["data"] is System.Collections.IEnumerable dataList) + { + companies = new List(); + foreach (var item in dataList) + { + if (item is IDictionary companyDict) + { + companies.Add(new CompanyDto + { + Id = (int)(dynamic)companyDict["id"], + CompanyCode = (string)companyDict["companyCode"], + CompanyName = (string)companyDict["companyName"], + ContactPerson = (string?)companyDict["contactPerson"], + Phone = (string?)companyDict["phone"], + Email = (string?)companyDict["email"], + IsActive = (bool)(dynamic)companyDict["isActive"], + CreatedAt = DateTime.Parse(companyDict["createdAt"].ToString()!) + }); + } + } + } + } + } + catch (Exception ex) + { + Snackbar.Add($"고객사 로드 실패: {ex.Message}", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private async Task NextPage() + { + currentPage++; + await LoadData(); + } + + private async Task PreviousPage() + { + currentPage = Math.Max(1, currentPage - 1); + await LoadData(); + } + + private class CompanyDto + { + public int Id { get; set; } + public string CompanyCode { get; set; } = ""; + public string CompanyName { get; set; } = ""; + public string? ContactPerson { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public bool IsActive { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/TaxBaik.Web/Controllers/CompanyController.cs b/TaxBaik.Web/Controllers/CompanyController.cs new file mode 100644 index 0000000..373c352 --- /dev/null +++ b/TaxBaik.Web/Controllers/CompanyController.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class CompanyController(CompanyService companyService) : ControllerBase +{ + [HttpGet("{id:int}")] + public async Task GetById(int id) + { + try + { + var company = await companyService.GetByIdAsync(id); + if (company == null) + return NotFound(new ProblemDetails { Title = "회사를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + return Ok(company); + } + catch (Exception ex) + { + return StatusCode(500, new ProblemDetails { Title = "회사 조회 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError }); + } + } + + [HttpGet("code/{code}")] + public async Task GetByCode(string code) + { + try + { + var company = await companyService.GetByCodeAsync(code); + if (company == null) + return NotFound(new ProblemDetails { Title = "회사를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + return Ok(company); + } + catch (Exception ex) + { + return StatusCode(500, new ProblemDetails { Title = "회사 조회 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError }); + } + } + + [HttpGet] + public async Task GetPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 20) + { + try + { + var (companies, total) = await companyService.GetPagedAsync(page, pageSize); + return Ok(new { data = companies, total, page, pageSize }); + } + catch (Exception ex) + { + return StatusCode(500, new ProblemDetails { Title = "회사 목록 조회 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError }); + } + } + + [HttpPost] + public async Task Create([FromBody] CreateCompanyRequest request) + { + try + { + var id = await companyService.CreateAsync( + request.CompanyCode, request.CompanyName, request.ContactPerson, + request.Phone, request.Email, request.Memo); + return CreatedAtAction(nameof(GetById), new { id }, new { message = "회사가 등록되었습니다.", id }); + } + catch (ValidationException ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + catch (Exception ex) + { + return StatusCode(500, new ProblemDetails { Title = "회사 등록 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError }); + } + } + + [HttpPut("{id:int}")] + public async Task Update(int id, [FromBody] UpdateCompanyRequest request) + { + try + { + await companyService.UpdateAsync(id, request.CompanyCode, request.CompanyName, + request.ContactPerson, request.Phone, request.Email, request.Memo, request.IsActive); + return Ok(new { message = "회사가 수정되었습니다." }); + } + catch (ValidationException ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + catch (Exception ex) + { + return StatusCode(500, new ProblemDetails { Title = "회사 수정 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError }); + } + } + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + try + { + await companyService.DeleteAsync(id); + return Ok(new { message = "회사가 삭제되었습니다." }); + } + catch (ValidationException ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + catch (Exception ex) + { + return StatusCode(500, new ProblemDetails { Title = "회사 삭제 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError }); + } + } + + public record CreateCompanyRequest(string CompanyCode, string CompanyName, string? ContactPerson, string? Phone, string? Email, string? Memo); + public record UpdateCompanyRequest(string CompanyCode, string CompanyName, string? ContactPerson, string? Phone, string? Email, string? Memo, bool IsActive); +} diff --git a/db/migrations/V014__CreateCompaniesTable.sql b/db/migrations/V014__CreateCompaniesTable.sql new file mode 100644 index 0000000..041bb6b --- /dev/null +++ b/db/migrations/V014__CreateCompaniesTable.sql @@ -0,0 +1,30 @@ +-- Create Companies table for multi-tenant support +CREATE TABLE IF NOT EXISTS companies ( + id SERIAL PRIMARY KEY, + company_code VARCHAR(50) NOT NULL UNIQUE, + company_name VARCHAR(200) NOT NULL, + contact_person VARCHAR(100), + phone VARCHAR(20), + email VARCHAR(200), + memo TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Add company_id to admin_users (nullable for backward compatibility) +ALTER TABLE admin_users ADD COLUMN IF NOT EXISTS company_id INT REFERENCES companies(id) ON DELETE RESTRICT; + +-- Create index for company lookups +CREATE INDEX IF NOT EXISTS idx_companies_code ON companies(company_code); +CREATE INDEX IF NOT EXISTS idx_admin_users_company ON admin_users(company_id); + +-- Insert default company for existing admin users +INSERT INTO companies (company_code, company_name, is_active) +VALUES ('DEFAULT', '기본 회사', TRUE) +ON CONFLICT (company_code) DO NOTHING; + +-- Assign existing admin users to default company if not assigned +UPDATE admin_users +SET company_id = (SELECT id FROM companies WHERE company_code = 'DEFAULT') +WHERE company_id IS NULL;