Merge pull request 'fix(대무자): COMPANY_ADMIN + SQL + UI select' from johngreen into main
Build & Deploy to K8s / build-and-deploy (push) Failing after 3m24s
Build & Deploy to K8s / build-and-deploy (push) Failing after 3m24s
This commit is contained in:
@@ -53,7 +53,7 @@ public class SubstituteController {
|
|||||||
@PathVariable("id") Long substituteId,
|
@PathVariable("id") Long substituteId,
|
||||||
@RequestAttribute("company_code") String companyCode,
|
@RequestAttribute("company_code") String companyCode,
|
||||||
@RequestAttribute("role") String role) {
|
@RequestAttribute("role") String role) {
|
||||||
if (!"ADMIN".equals(role) && !"SUPER_ADMIN".equals(role)) {
|
if (!"ADMIN".equals(role) && !"COMPANY_ADMIN".equals(role) && !"SUPER_ADMIN".equals(role)) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
.body(ApiResponse.error("관리자만 조회할 수 있습니다."));
|
.body(ApiResponse.error("관리자만 조회할 수 있습니다."));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ public class SubstituteService extends BaseService {
|
|||||||
|
|
||||||
private void requireAdmin(Map<String, Object> params) {
|
private void requireAdmin(Map<String, Object> params) {
|
||||||
String role = (String) params.get("role");
|
String role = (String) params.get("role");
|
||||||
if (!"ADMIN".equals(role) && !"SUPER_ADMIN".equals(role)) {
|
if (!"ADMIN".equals(role) && !"COMPANY_ADMIN".equals(role) && !"SUPER_ADMIN".equals(role)) {
|
||||||
throw new AccessDeniedException("관리자만 대무자를 지정/수정/해지할 수 있습니다.");
|
throw new AccessDeniedException("관리자만 대무자를 지정/수정/해지할 수 있습니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
AND L.COMPANY_CODE = R.COMPANY_CODE
|
AND L.COMPANY_CODE = R.COMPANY_CODE
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
ORDER BY R.CREATED_DATE DESC
|
ORDER BY R.CREATED_AT DESC
|
||||||
<if test="page_limit != null">
|
<if test="page_limit != null">
|
||||||
LIMIT #{page_limit} OFFSET #{page_offset}
|
LIMIT #{page_limit} OFFSET #{page_offset}
|
||||||
</if>
|
</if>
|
||||||
@@ -465,7 +465,7 @@
|
|||||||
SELECT L.*,
|
SELECT L.*,
|
||||||
R.TITLE, R.TARGET_TABLE, R.TARGET_RECORD_ID,
|
R.TITLE, R.TARGET_TABLE, R.TARGET_RECORD_ID,
|
||||||
R.REQUESTER_NAME, R.REQUESTER_DEPT,
|
R.REQUESTER_NAME, R.REQUESTER_DEPT,
|
||||||
R.CREATED_DATE AS REQUEST_CREATED_DATE
|
R.CREATED_AT AS REQUEST_CREATED_DATE
|
||||||
FROM APPROVAL_LINES L
|
FROM APPROVAL_LINES L
|
||||||
JOIN APPROVAL_REQUESTS R
|
JOIN APPROVAL_REQUESTS R
|
||||||
ON L.REQUEST_ID = R.REQUEST_ID AND L.COMPANY_CODE = R.COMPANY_CODE
|
ON L.REQUEST_ID = R.REQUEST_ID AND L.COMPANY_CODE = R.COMPANY_CODE
|
||||||
@@ -475,7 +475,7 @@
|
|||||||
</foreach>
|
</foreach>
|
||||||
AND L.STATUS = 'pending'
|
AND L.STATUS = 'pending'
|
||||||
AND (L.COMPANY_CODE = #{company_code} OR L.COMPANY_CODE = '*')
|
AND (L.COMPANY_CODE = #{company_code} OR L.COMPANY_CODE = '*')
|
||||||
ORDER BY R.CREATED_DATE ASC
|
ORDER BY R.CREATED_AT ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- ================================================================
|
<!-- ================================================================
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
getSubstituteList,
|
getSubstituteList,
|
||||||
createSubstitute,
|
createSubstitute,
|
||||||
deleteSubstitute,
|
deleteSubstitute,
|
||||||
checkSubstituteOverlap,
|
checkSubstituteOverlap,
|
||||||
} from "@/lib/api/substitute";
|
} from "@/lib/api/substitute";
|
||||||
|
import { getUserList } from "@/lib/api/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자별 대무자(代務者) 관리 섹션.
|
* 사용자별 대무자(代務者) 관리 섹션.
|
||||||
@@ -39,6 +47,10 @@ export function SubstituteSection({ originalUserId, originalUserName }: Substitu
|
|||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// 같은 회사 활성 사용자 목록 (대무자 후보) — 본인/SUPER_ADMIN/비활성 제외
|
||||||
|
const [candidates, setCandidates] = useState<Record<string, any>[]>([]);
|
||||||
|
const [candidatesLoading, setCandidatesLoading] = useState(false);
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
proxy_user_id: "",
|
proxy_user_id: "",
|
||||||
start_date: "",
|
start_date: "",
|
||||||
@@ -63,10 +75,29 @@ export function SubstituteSection({ originalUserId, originalUserName }: Substitu
|
|||||||
load();
|
load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
|
// 대무자 후보 사용자 목록 로드 (다이얼로그 열릴 때 한번)
|
||||||
|
const loadCandidates = useCallback(async () => {
|
||||||
|
setCandidatesLoading(true);
|
||||||
|
const res = await getUserList({ status: "active", limit: 1000 });
|
||||||
|
if (res?.success && Array.isArray(res.data?.list)) {
|
||||||
|
const filtered = (res.data.list as Record<string, any>[]).filter(
|
||||||
|
(u) =>
|
||||||
|
u.user_id !== originalUserId &&
|
||||||
|
u.user_type !== "SUPER_ADMIN" &&
|
||||||
|
u.status === "active",
|
||||||
|
);
|
||||||
|
setCandidates(filtered);
|
||||||
|
} else {
|
||||||
|
setCandidates([]);
|
||||||
|
}
|
||||||
|
setCandidatesLoading(false);
|
||||||
|
}, [originalUserId]);
|
||||||
|
|
||||||
const openDialog = () => {
|
const openDialog = () => {
|
||||||
setForm({ proxy_user_id: "", start_date: "", end_date: "", reason: "" });
|
setForm({ proxy_user_id: "", start_date: "", end_date: "", reason: "" });
|
||||||
setError(null);
|
setError(null);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
|
loadCandidates();
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@@ -215,16 +246,36 @@ export function SubstituteSection({ originalUserId, originalUserName }: Substitu
|
|||||||
|
|
||||||
<div className="space-y-3 py-2 text-sm">
|
<div className="space-y-3 py-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="proxy_user_id">대무자 사용자 ID</Label>
|
<Label htmlFor="proxy_user_id">대무자</Label>
|
||||||
<Input
|
<Select
|
||||||
id="proxy_user_id"
|
|
||||||
value={form.proxy_user_id}
|
value={form.proxy_user_id}
|
||||||
onChange={(e) => setForm({ ...form, proxy_user_id: e.target.value })}
|
onValueChange={(v) => setForm({ ...form, proxy_user_id: v })}
|
||||||
placeholder="예: hjkim"
|
>
|
||||||
autoFocus
|
<SelectTrigger id="proxy_user_id" autoFocus>
|
||||||
/>
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
candidatesLoading
|
||||||
|
? "사용자 목록 불러오는 중..."
|
||||||
|
: candidates.length === 0
|
||||||
|
? "지정 가능한 사용자가 없습니다"
|
||||||
|
: "대무자를 선택하세요"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{candidates.map((u) => (
|
||||||
|
<SelectItem key={u.user_id} value={u.user_id}>
|
||||||
|
{u.user_name || u.user_id}
|
||||||
|
<span className="ml-1 text-muted-foreground">
|
||||||
|
({u.user_id}
|
||||||
|
{u.dept_name ? ` · ${u.dept_name}` : ""})
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<p className="mt-1 text-[0.7rem] text-muted-foreground">
|
<p className="mt-1 text-[0.7rem] text-muted-foreground">
|
||||||
같은 회사 사용자만 지정 가능. SUPER_ADMIN 은 지정 불가.
|
같은 회사 활성 사용자만 표시. 본인·SUPER_ADMIN 자동 제외.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user