admin/userMng 전체폭 사용 + 부서관리 dept_info 스키마 풀 매핑
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m27s

화면 폭:
- 4개 페이지(사용자/권한 그룹/권한/부서) max-w 제거 → 화면 전체 사용
- 헤더 텍스트 컴팩트화 (text-xl + 작은 설명문)

부서관리 dept_info 스키마 1:1 매핑:
- 누락 필드 6개 추가: master_sabun, master_user_id, location, location_name, data_type, sales_yn
- frontend types/department.ts: Department / DepartmentFormData 확장
- frontend deptMngList: 폼 추가 + handleSelectDepartment / payload 매핑
- backend mapper: INSERT/UPDATE에 6개 컬럼 추가
- backend DepartmentService: insertParams/updateParams에 6개 필드 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-04-27 17:20:46 +09:00
parent b1971d9107
commit 30ab45a3c6
7 changed files with 206 additions and 33 deletions
@@ -92,6 +92,13 @@ public class DepartmentService extends BaseService {
insertParams.put("show_in_chart", bodyParam(body, "show_in_chart", "show_in_chart"));
insertParams.put("sort_order", bodyParam(body, "sort_order", "sort_order"));
insertParams.put("status", bodyParam(body, "status", "status"));
// dept_info 추가 필드 (master_*, location_*, data_type, sales_yn)
insertParams.put("master_sabun", nullIfBlank(bodyParam(body, "master_sabun", "master_sabun")));
insertParams.put("master_user_id", nullIfBlank(bodyParam(body, "master_user_id", "master_user_id")));
insertParams.put("location", nullIfBlank(bodyParam(body, "location", "location")));
insertParams.put("location_name", nullIfBlank(bodyParam(body, "location_name", "location_name")));
insertParams.put("data_type", bodyParam(body, "data_type", "data_type"));
insertParams.put("sales_yn", bodyParam(body, "sales_yn", "sales_yn"));
sqlSession.insert("department.insertDepartment", insertParams);
log.info("부서 생성 성공: deptCode={}, deptName={}", deptCode, deptName);
@@ -127,6 +134,13 @@ public class DepartmentService extends BaseService {
params.put("show_in_chart", bodyParam(body, "show_in_chart", "show_in_chart"));
params.put("sort_order", bodyParam(body, "sort_order", "sort_order"));
params.put("status", bodyParam(body, "status", "status"));
// dept_info 추가 필드
params.put("master_sabun", nullIfBlank(bodyParam(body, "master_sabun", "master_sabun")));
params.put("master_user_id", nullIfBlank(bodyParam(body, "master_user_id", "master_user_id")));
params.put("location", nullIfBlank(bodyParam(body, "location", "location")));
params.put("location_name", nullIfBlank(bodyParam(body, "location_name", "location_name")));
params.put("data_type", bodyParam(body, "data_type", "data_type"));
params.put("sales_yn", bodyParam(body, "sales_yn", "sales_yn"));
int updated = sqlSession.update("department.updateDepartment", params);
if (updated == 0) {
@@ -85,7 +85,7 @@
WHERE DEPT_CODE ~ '^DEPT_[0-9]+$'
</select>
<!-- 부서 생성 -->
<!-- 부서 생성 (dept_info 스키마 1:1) -->
<insert id="insertDepartment" parameterType="map">
INSERT INTO DEPT_INFO (
DEPT_CODE,
@@ -108,6 +108,12 @@
SHOW_IN_CHART,
SORT_ORDER,
STATUS,
MASTER_SABUN,
MASTER_USER_ID,
LOCATION,
LOCATION_NAME,
DATA_TYPE,
SALES_YN,
CREATED_DATE
) VALUES (
#{dept_code},
@@ -130,11 +136,17 @@
COALESCE(#{show_in_chart}, 'Y'),
COALESCE(#{sort_order}, 10),
COALESCE(#{status}, 'active'),
#{master_sabun},
#{master_user_id},
#{location},
#{location_name},
COALESCE(#{data_type}, 'real'),
COALESCE(#{sales_yn}, 'N'),
NOW()
)
</insert>
<!-- 부서 수정 -->
<!-- 부서 수정 (dept_info 스키마 1:1) -->
<update id="updateDepartment" parameterType="map">
UPDATE DEPT_INFO
SET
@@ -154,7 +166,13 @@
ERP_MANAGED = #{erp_managed},
SHOW_IN_CHART = #{show_in_chart},
SORT_ORDER = #{sort_order},
STATUS = #{status}
STATUS = #{status},
MASTER_SABUN = #{master_sabun},
MASTER_USER_ID = #{master_user_id},
LOCATION = #{location},
LOCATION_NAME = #{location_name},
DATA_TYPE = #{data_type},
SALES_YN = #{sales_yn}
WHERE DEPT_CODE = #{dept_code}
</update>
@@ -33,20 +33,58 @@ import { getCompanyList } from "@/lib/api/company";
import type { Department, DepartmentMember } from "@/types/department";
import type { Company } from "@/types/company";
/**
* dept_info 테이블 스키마에 1:1 매핑되는 Draft.
*
* 필드 매핑:
* dept_code : 부서코드 (PK)
* parent_dept_code : 상위부서코드
* dept_name : 부서명
* master_sabun : 부서장 사번
* master_user_id : 부서장 사용자ID
* location : 위치코드
* location_name : 위치명
* data_type : 데이터 구분 (정/임시)
* status : 사용여부 (active/inactive)
* sales_yn : 영업조직 여부 (Y/N)
* company_name : 회사명 (조회용)
* company_code : 회사코드
* short_name : 부서약칭
* dept_type : 부서유형 (dept|team|temp)
* org_system : 조직체계
* approval_manager : 결재 관리자
* dept_manager : 부서 관리자
* org_head : 조직장
* zipcode : 우편번호
* address1 : 주소1
* address2 : 주소2
* start_date : 시작일
* end_date : 종료일
* erp_managed : ERP 관리부서 (Y/N)
* show_in_chart : 조직도 표시 (Y/N)
* sort_order : 정렬순서
*/
interface DeptDetailDraft {
dept_code: string;
dept_name: string;
parent_dept_code: string | null;
dept_name: string;
master_sabun: string;
master_user_id: string;
location: string;
location_name: string;
data_type: string;
status: "active" | "inactive";
sales_yn: "Y" | "N";
company_code: string;
short_name: string;
dept_type: string;
org_system: string;
short_name: string;
approval_manager: string;
dept_manager: string;
org_head: string;
zipcode: string;
address1: string;
address2: string;
status: "active" | "inactive";
start_date: string;
end_date: string;
erp_managed: "Y" | "N";
@@ -56,18 +94,25 @@ interface DeptDetailDraft {
const emptyDraft = (companyCode = ""): DeptDetailDraft => ({
dept_code: "",
dept_name: "",
parent_dept_code: null,
dept_name: "",
master_sabun: "",
master_user_id: "",
location: "",
location_name: "",
data_type: "real",
status: "active",
sales_yn: "N",
company_code: companyCode,
short_name: "",
dept_type: "dept",
org_system: "",
short_name: "",
approval_manager: "",
dept_manager: "",
org_head: "",
zipcode: "",
address1: "",
address2: "",
status: "active",
start_date: new Date().toISOString().slice(0, 10),
end_date: "",
erp_managed: "Y",
@@ -193,15 +238,23 @@ export default function DeptMngListPage() {
});
};
// ── 선택 시 상세정보 채우기 ───────────────────────────
// ── 선택 시 상세정보 채우기 (dept_info 스키마 1:1) ────
const handleSelectDepartment = (dept: Department) => {
setSelectedCode(dept.dept_code);
setIsNewMode(false);
const d = dept as any;
const loaded: DeptDetailDraft = {
...emptyDraft(selectedCompanyCode),
dept_code: dept.dept_code,
dept_name: dept.dept_name,
parent_dept_code: dept.parent_dept_code ?? null,
dept_name: dept.dept_name,
master_sabun: d.master_sabun ?? "",
master_user_id: d.master_user_id ?? "",
location: d.location ?? "",
location_name: d.location_name ?? "",
data_type: d.data_type ?? "real",
sales_yn: ((d.sales_yn as "Y" | "N") ?? "N"),
company_code: dept.company_code ?? selectedCompanyCode,
short_name: dept.short_name ?? "",
dept_type: dept.dept_type ?? "dept",
org_system: dept.org_system ?? "",
@@ -248,7 +301,7 @@ export default function DeptMngListPage() {
return;
}
// 기본정보 탭 전체 필드를 payload 로 전달 — DepartmentFormData 와 1:1
// 기본정보 탭 전체 필드를 payload 로 전달 — dept_info 스키마와 1:1
const payload = {
dept_name: draft.dept_name,
parent_dept_code: draft.parent_dept_code,
@@ -267,7 +320,14 @@ export default function DeptMngListPage() {
show_in_chart: draft.show_in_chart,
sort_order: draft.sort_order,
status: draft.status,
};
// dept_info 추가 필드
master_sabun: draft.master_sabun,
master_user_id: draft.master_user_id,
location: draft.location,
location_name: draft.location_name,
data_type: draft.data_type,
sales_yn: draft.sales_yn,
} as any;
try {
if (isNewMode) {
@@ -321,7 +381,7 @@ export default function DeptMngListPage() {
// 렌더
// ─────────────────────────────────────────────────────
return (
<div className="mx-auto flex h-full min-h-0 w-full max-w-[1700px] flex-col bg-background text-sm">
<div className="flex h-full min-h-0 w-full flex-col bg-background text-sm">
{/* 상단 타이틀 바 */}
<div className="flex items-center justify-between border-b px-5 py-3">
<div className="flex items-center gap-2">
@@ -990,6 +1050,75 @@ function BasicInfoForm({
className="h-8 text-sm"
/>
</Row>
<Row label="부서장 사번" hint>
<Input
value={draft.master_sabun}
onChange={(e) => update("master_sabun", e.target.value)}
className="h-8 text-sm"
placeholder="부서장 사번"
/>
</Row>
<Row label="부서장 사용자ID">
<Input
value={draft.master_user_id}
onChange={(e) => update("master_user_id", e.target.value)}
className="h-8 text-sm"
placeholder="user_id"
/>
</Row>
<Row label="위치코드">
<div className="grid grid-cols-2 gap-2">
<Input
value={draft.location}
onChange={(e) => update("location", e.target.value)}
className="h-8 text-sm"
placeholder="위치코드"
/>
<Input
value={draft.location_name}
onChange={(e) => update("location_name", e.target.value)}
className="h-8 text-sm"
placeholder="위치명"
/>
</div>
</Row>
<Row label="데이터구분" hint>
<RadioGroup
value={draft.data_type}
onValueChange={(v) => update("data_type", v)}
className="flex items-center gap-4"
>
<div className="flex items-center gap-1">
<RadioGroupItem value="real" id="dtype-real" className="h-3.5 w-3.5" />
<Label htmlFor="dtype-real" className="text-sm"></Label>
</div>
<div className="flex items-center gap-1">
<RadioGroupItem value="temp" id="dtype-temp" className="h-3.5 w-3.5" />
<Label htmlFor="dtype-temp" className="text-sm"></Label>
</div>
</RadioGroup>
</Row>
<Row label="영업조직 여부">
<RadioGroup
value={draft.sales_yn}
onValueChange={(v) => update("sales_yn", v as "Y" | "N")}
className="flex items-center gap-4"
>
<div className="flex items-center gap-1">
<RadioGroupItem value="Y" id="sales-y" className="h-3.5 w-3.5" />
<Label htmlFor="sales-y" className="text-sm"></Label>
</div>
<div className="flex items-center gap-1">
<RadioGroupItem value="N" id="sales-n" className="h-3.5 w-3.5" />
<Label htmlFor="sales-n" className="text-sm"></Label>
</div>
</RadioGroup>
</Row>
</div>
</div>
);
@@ -557,7 +557,7 @@ export default function RolesPage() {
if (!isAdmin) {
return (
<div className="flex min-h-screen flex-col bg-background">
<div className="mx-auto w-full max-w-6xl space-y-4 p-6">
<div className="w-full space-y-4 p-6">
<div className="space-y-1 border-b pb-3">
<h1 className="text-2xl font-bold tracking-tight"> </h1>
</div>
@@ -579,10 +579,9 @@ export default function RolesPage() {
return (
<div className="flex min-h-screen flex-col bg-background">
{/* 풀HD 기준 너무 크지 않도록 max-w 제한 */}
<div className="mx-auto w-full max-w-[1600px] space-y-3 p-4 md:p-6">
<div className="w-full space-y-3 p-6">
<div className="space-y-1 border-b pb-3">
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<h1 className="text-xl font-bold tracking-tight"> </h1>
<p className="text-muted-foreground text-xs">
/ , .
</p>
@@ -171,7 +171,7 @@ export default function RolesPage() {
if (!isAdmin) {
return (
<div className="flex min-h-screen flex-col bg-background">
<div className="mx-auto w-full max-w-6xl space-y-4 p-6">
<div className="w-full space-y-4 p-6">
<div className="space-y-1 border-b pb-3">
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<p className="text-xs text-muted-foreground"> ( )</p>
@@ -196,12 +196,11 @@ export default function RolesPage() {
return (
<div className="flex min-h-screen flex-col bg-background">
{/* 풀HD 기준 너무 크지 않도록 max-w 제한 */}
<div className="mx-auto w-full max-w-7xl space-y-4 p-4 md:p-6">
<div className="w-full space-y-4 p-6">
{/* 페이지 헤더 */}
<div className="flex items-end justify-between border-b pb-3">
<div className="space-y-0.5">
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<h1 className="text-xl font-bold tracking-tight"> </h1>
<p className="text-xs text-muted-foreground"> ( )</p>
</div>
<Button onClick={handleCreateRole} size="sm" className="gap-1.5 h-9">
@@ -279,7 +278,7 @@ export default function RolesPage() {
</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
{filteredRoleGroups.map((role) => (
<div
key={role.objid}
@@ -110,11 +110,10 @@ export default function UserMngPage() {
return (
<div className="flex min-h-screen flex-col bg-background">
{/* 풀HD 기준 너무 크지 않도록 max-w 제한 */}
<div className="mx-auto w-full max-w-7xl space-y-4 p-4 md:p-6">
<div className="w-full space-y-4 p-6">
{/* 페이지 헤더 */}
<div className="space-y-1 border-b pb-3">
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<h1 className="text-xl font-bold tracking-tight"> </h1>
<p className="text-xs text-muted-foreground"> </p>
</div>
+22 -7
View File
@@ -2,13 +2,21 @@
* 부서 관리 관련 타입 정의
*/
// 부서 정보 (dept_info 테이블 기반)
// 부서 정보 (dept_info 테이블 1:1 매핑)
export interface Department {
dept_code: string; // 부서 코드
dept_name: string; // 부서명
company_code: string; // 회사 코드
dept_code: string; // 부서 코드 (PK)
parent_dept_code?: string | null; // 상위 부서 코드
// dept_info 확장 컬럼
dept_name: string; // 부서명
master_sabun?: string | null; // 부서장 사번
master_user_id?: string | null; // 부서장 사용자ID
location?: string | null; // 위치코드
location_name?: string | null; // 위치명
created_date?: string | null; // 생성일시
data_type?: string | null; // 데이터 구분 (real/temp)
status?: "active" | "inactive" | null; // 사용여부
sales_yn?: "Y" | "N" | null; // 영업조직 여부
company_name?: string | null; // 회사명
company_code: string; // 회사 코드
short_name?: string | null; // 부서약칭
dept_type?: string | null; // 부서유형 (dept/team/temp)
org_system?: string | null; // 조직체계
@@ -23,7 +31,6 @@ export interface Department {
erp_managed?: "Y" | "N" | null;
show_in_chart?: "Y" | "N" | null;
sort_order?: number | null;
status?: "active" | "inactive" | null;
created_at?: string;
updated_at?: string;
// UI용 추가 필드
@@ -52,7 +59,7 @@ export interface UserDepartmentMapping {
created_at?: string;
}
// 부서 등록/수정 폼 데이터 — 기본정보 탭 모든 필드 전달 가능
// 부서 등록/수정 폼 데이터 — dept_info 스키마 1:1
export interface DepartmentFormData {
dept_name: string; // 부서명 (필수)
parent_dept_code?: string | null;
@@ -71,6 +78,14 @@ export interface DepartmentFormData {
show_in_chart?: "Y" | "N" | null;
sort_order?: number | null;
status?: "active" | "inactive" | null;
// dept_info 추가 필드
master_sabun?: string | null;
master_user_id?: string | null;
location?: string | null;
location_name?: string | null;
data_type?: string | null;
sales_yn?: "Y" | "N" | null;
dept_code?: string | null; // 일괄등록용 (자동 부여 시 미전달)
}
// 부서 트리 노드 (UI용)