feat(부서관리): 기준일 필터 + 시작일/종료일 UI 노출
- 백엔드: selectDepartments 에 base_date <if> 블록 추가 (start_date <= base_date AND (end_date IS NULL OR end_date >= base_date))
- 서비스/컨트롤러에 3-arg overload 와 @RequestParam("base_date") 추가
- 프론트: 사용기간 RadioGroup + 시작일/종료일 Row 의 {false &&} hide 제거
- loadDepartments 가 periodMode === "date" 일 때 baseDate 를 API 에 전달
This commit is contained in:
@@ -21,20 +21,22 @@ public class DepartmentController {
|
||||
/**
|
||||
* 부서 목록 조회 (회사별).
|
||||
* 기본은 active 부서만. ?include_deleted=true 시 soft-delete 된 부서도 포함.
|
||||
* GET /api/departments/companies/{companyCode}/departments[?include_deleted=true]
|
||||
* ?base_date=YYYY-MM-DD 시 해당 시점에 active 했던 부서만 반환.
|
||||
* GET /api/departments/companies/{companyCode}/departments[?include_deleted=true][&base_date=YYYY-MM-DD]
|
||||
*/
|
||||
@GetMapping("/companies/{companyCode}/departments")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getDepartments(
|
||||
@PathVariable String companyCode,
|
||||
@RequestAttribute("company_code") String userCompanyCode,
|
||||
@RequestParam(value = "include_deleted", required = false, defaultValue = "false") boolean includeDeleted) {
|
||||
@RequestParam(value = "include_deleted", required = false, defaultValue = "false") boolean includeDeleted,
|
||||
@RequestParam(value = "base_date", required = false) String baseDate) {
|
||||
|
||||
if (!isSuperAdmin(userCompanyCode) && !userCompanyCode.equals(companyCode)) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(ApiResponse.error("해당 회사의 부서를 조회할 권한이 없습니다."));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> departments = departmentService.getDepartments(companyCode, includeDeleted);
|
||||
List<Map<String, Object>> departments = departmentService.getDepartments(companyCode, includeDeleted, baseDate);
|
||||
return ResponseEntity.ok(ApiResponse.success(departments, "부서 목록 조회 성공"));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,17 +20,22 @@ public class DepartmentService extends BaseService {
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
public List<Map<String, Object>> getDepartments(String companyCode) {
|
||||
return getDepartments(companyCode, false);
|
||||
return getDepartments(companyCode, false, null);
|
||||
}
|
||||
|
||||
/** soft-delete 대응 — includeDeleted=true 면 DELETED_AT 부서도 포함 */
|
||||
public List<Map<String, Object>> getDepartments(String companyCode, boolean includeDeleted) {
|
||||
return getDepartments(companyCode, includeDeleted, null);
|
||||
}
|
||||
|
||||
/** 기준일 필터 — baseDate 가 있으면 해당 시점에 active 한 부서만 반환 (start_date ≤ baseDate ≤ end_date OR end_date IS NULL) */
|
||||
public List<Map<String, Object>> getDepartments(String companyCode, boolean includeDeleted, String baseDate) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("company_code", companyCode);
|
||||
params.put("include_deleted", includeDeleted);
|
||||
params.put("base_date", baseDate); // null/빈문자면 XML if 가 skip
|
||||
List<Map<String, Object>> departments = sqlSession.selectList("department.selectDepartments", params);
|
||||
|
||||
// member_count를 int로 변환
|
||||
for (Map<String, Object> dept : departments) {
|
||||
Object cnt = dept.get("member_count");
|
||||
if (cnt != null) {
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<if test="include_deleted == null or include_deleted == false">
|
||||
AND D.DELETED_AT IS NULL
|
||||
</if>
|
||||
<if test="base_date != null and base_date != ''">
|
||||
AND D.START_DATE <= #{base_date}::date
|
||||
AND (D.END_DATE IS NULL OR D.END_DATE >= #{base_date}::date)
|
||||
</if>
|
||||
GROUP BY
|
||||
D.DEPT_CODE, D.DEPT_NAME, D.COMPANY_CODE, D.PARENT_DEPT_CODE,
|
||||
D.SHORT_NAME, D.DEPT_TYPE, D.ORG_SYSTEM, D.APPROVAL_MANAGER, D.DEPT_MANAGER,
|
||||
|
||||
@@ -201,7 +201,10 @@ export default function DeptMngListPage() {
|
||||
if (!selectedCompanyCode) return;
|
||||
setIsTreeLoading(true);
|
||||
try {
|
||||
const res = await departmentAPI.getDepartments(selectedCompanyCode, { includeDeleted: showDeleted });
|
||||
const res = await departmentAPI.getDepartments(selectedCompanyCode, {
|
||||
includeDeleted: showDeleted,
|
||||
baseDate: periodMode === "date" ? baseDate : undefined,
|
||||
});
|
||||
if (res.success && (res as any).data) {
|
||||
setDepartments((res as any).data);
|
||||
} else {
|
||||
@@ -210,7 +213,7 @@ export default function DeptMngListPage() {
|
||||
} finally {
|
||||
setIsTreeLoading(false);
|
||||
}
|
||||
}, [selectedCompanyCode, showDeleted]);
|
||||
}, [selectedCompanyCode, showDeleted, periodMode, baseDate]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDepartments();
|
||||
@@ -645,33 +648,30 @@ export default function DeptMngListPage() {
|
||||
<aside className="flex w-[340px] shrink-0 flex-col border-r">
|
||||
{/* 기준일 / 회사 / 검색 */}
|
||||
<div className="space-y-3 border-b p-3">
|
||||
{/* TODO V2: 사용기간 필터 — backend 미구현, V1 hidden */}
|
||||
{false && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="w-[60px] shrink-0 text-xs font-semibold">사용기간</Label>
|
||||
<RadioGroup
|
||||
value={periodMode}
|
||||
onValueChange={(v) => setPeriodMode(v as "all" | "date")}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="all" id="period-all" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="period-all" className="text-xs">전체</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="date" id="period-date" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="period-date" className="text-xs">기준일</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Input
|
||||
type="date"
|
||||
value={baseDate}
|
||||
onChange={(e) => setBaseDate(e.target.value)}
|
||||
disabled={periodMode !== "date"}
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="w-[60px] shrink-0 text-xs font-semibold">사용기간</Label>
|
||||
<RadioGroup
|
||||
value={periodMode}
|
||||
onValueChange={(v) => setPeriodMode(v as "all" | "date")}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="all" id="period-all" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="period-all" className="text-xs">전체</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<RadioGroupItem value="date" id="period-date" className="h-3.5 w-3.5" />
|
||||
<Label htmlFor="period-date" className="text-xs">기준일</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Input
|
||||
type="date"
|
||||
value={baseDate}
|
||||
onChange={(e) => setBaseDate(e.target.value)}
|
||||
disabled={periodMode !== "date"}
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={selectedCompanyCode}
|
||||
@@ -1423,28 +1423,25 @@ function BasicInfoForm({
|
||||
</RadioGroup>
|
||||
</Row>
|
||||
|
||||
{/* TODO V2: 사용기간 (시작일/종료일) — 필터 도입 시 사용. 컬럼은 DEPT_INFO.START_DATE/END_DATE 유지 */}
|
||||
{false && (
|
||||
<Row label="시작일">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Row label="시작일">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
type="date"
|
||||
value={draft.start_date}
|
||||
onChange={(e) => update("start_date", e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-[50px] text-xs">종료일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={draft.start_date}
|
||||
onChange={(e) => update("start_date", e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
value={draft.end_date}
|
||||
onChange={(e) => update("end_date", e.target.value)}
|
||||
className="h-8 flex-1 text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-[50px] text-xs">종료일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={draft.end_date}
|
||||
onChange={(e) => update("end_date", e.target.value)}
|
||||
className="h-8 flex-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Row label="정렬">
|
||||
<Input
|
||||
|
||||
@@ -8,15 +8,19 @@ import { Department, DepartmentMember, DepartmentFormData } from "@/types/depart
|
||||
/**
|
||||
* 부서 목록 조회 (회사별).
|
||||
* options.includeDeleted=true 시 soft-delete 된 부서도 포함.
|
||||
* options.baseDate (YYYY-MM-DD) 가 있으면 해당 시점에 active 한 부서만 반환.
|
||||
*/
|
||||
export async function getDepartments(
|
||||
companyCode: string,
|
||||
options?: { includeDeleted?: boolean },
|
||||
options?: { includeDeleted?: boolean; baseDate?: string },
|
||||
) {
|
||||
try {
|
||||
const url = `/departments/companies/${companyCode}/departments`;
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.includeDeleted) params.include_deleted = true;
|
||||
if (options?.baseDate) params.base_date = options.baseDate;
|
||||
const response = await apiClient.get<{ success: boolean; data: Department[] }>(url, {
|
||||
params: options?.includeDeleted ? { include_deleted: true } : undefined,
|
||||
params: Object.keys(params).length > 0 ? params : undefined,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
|
||||
Reference in New Issue
Block a user