refactor: update production controller to use AuthenticatedRequest

- Modified the production controller to replace the generic Request type with AuthenticatedRequest for better type safety and to ensure user authentication is handled correctly.
- This change enhances the security and clarity of the API endpoints related to production plan management, ensuring that user-specific data is accessed appropriately.

Made-with: Cursor
This commit is contained in:
kjs 2026-03-16 09:35:23 +09:00
parent ba39ebf341
commit 79e3420ff3
5 changed files with 60 additions and 59 deletions

View File

@ -2,13 +2,14 @@
*
*/
import { Request, Response } from "express";
import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import * as productionService from "../services/productionPlanService";
import { logger } from "../utils/logger";
// ─── 수주 데이터 조회 (품목별 그룹핑) ───
export async function getOrderSummary(req: Request, res: Response) {
export async function getOrderSummary(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const { excludePlanned, itemCode, itemName } = req.query;
@ -28,7 +29,7 @@ export async function getOrderSummary(req: Request, res: Response) {
// ─── 안전재고 부족분 조회 ───
export async function getStockShortage(req: Request, res: Response) {
export async function getStockShortage(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const data = await productionService.getStockShortage(companyCode);
@ -41,7 +42,7 @@ export async function getStockShortage(req: Request, res: Response) {
// ─── 생산계획 상세 조회 ───
export async function getPlanById(req: Request, res: Response) {
export async function getPlanById(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const planId = parseInt(req.params.id, 10);
@ -59,7 +60,7 @@ export async function getPlanById(req: Request, res: Response) {
// ─── 생산계획 수정 ───
export async function updatePlan(req: Request, res: Response) {
export async function updatePlan(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const planId = parseInt(req.params.id, 10);
@ -78,7 +79,7 @@ export async function updatePlan(req: Request, res: Response) {
// ─── 생산계획 삭제 ───
export async function deletePlan(req: Request, res: Response) {
export async function deletePlan(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const planId = parseInt(req.params.id, 10);
@ -96,7 +97,7 @@ export async function deletePlan(req: Request, res: Response) {
// ─── 자동 스케줄 생성 ───
export async function generateSchedule(req: Request, res: Response) {
export async function generateSchedule(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const createdBy = req.user!.userId;
@ -116,7 +117,7 @@ export async function generateSchedule(req: Request, res: Response) {
// ─── 스케줄 병합 ───
export async function mergeSchedules(req: Request, res: Response) {
export async function mergeSchedules(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const mergedBy = req.user!.userId;
@ -142,7 +143,7 @@ export async function mergeSchedules(req: Request, res: Response) {
// ─── 반제품 계획 자동 생성 ───
export async function generateSemiSchedule(req: Request, res: Response) {
export async function generateSemiSchedule(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const createdBy = req.user!.userId;
@ -167,7 +168,7 @@ export async function generateSemiSchedule(req: Request, res: Response) {
// ─── 스케줄 분할 ───
export async function splitSchedule(req: Request, res: Response) {
export async function splitSchedule(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const splitBy = req.user!.userId;

View File

@ -226,11 +226,11 @@ export function TimelineSchedulerComponent({
// 디자인 모드 플레이스홀더
if (isDesignMode) {
return (
<div className="w-full h-full min-h-[200px] border-2 border-dashed border-muted-foreground/30 rounded-lg flex items-center justify-center bg-muted/10">
<div className="flex h-full min-h-[200px] w-full items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/30 bg-muted/10">
<div className="text-center text-muted-foreground">
<Calendar className="h-8 w-8 mx-auto mb-2" />
<p className="text-sm font-medium"> </p>
<p className="text-xs mt-1">
<Calendar className="mx-auto mb-2 h-6 w-6 sm:h-8 sm:w-8" />
<p className="text-xs font-medium sm:text-sm"> </p>
<p className="mt-1 text-[10px] sm:text-xs">
{config.selectedTable
? `테이블: ${config.selectedTable}`
: "테이블을 선택하세요"}
@ -244,12 +244,12 @@ export function TimelineSchedulerComponent({
if (isLoading) {
return (
<div
className="w-full flex items-center justify-center bg-muted/10 rounded-lg"
className="flex w-full items-center justify-center rounded-lg bg-muted/10"
style={{ height: config.height || 500 }}
>
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
<span className="text-sm"> ...</span>
<Loader2 className="h-4 w-4 animate-spin sm:h-5 sm:w-5" />
<span className="text-xs sm:text-sm"> ...</span>
</div>
</div>
);
@ -259,12 +259,12 @@ export function TimelineSchedulerComponent({
if (error) {
return (
<div
className="w-full flex items-center justify-center bg-destructive/10 rounded-lg"
className="flex w-full items-center justify-center rounded-lg bg-destructive/10"
style={{ height: config.height || 500 }}
>
<div className="text-center text-destructive">
<p className="text-sm font-medium"> </p>
<p className="text-xs mt-1">{error}</p>
<p className="text-xs font-medium sm:text-sm"> </p>
<p className="mt-1 text-[10px] sm:text-xs">{error}</p>
</div>
</div>
);
@ -274,13 +274,13 @@ export function TimelineSchedulerComponent({
if (schedules.length === 0) {
return (
<div
className="w-full flex items-center justify-center bg-muted/10 rounded-lg border"
className="flex w-full items-center justify-center rounded-lg border bg-muted/10"
style={{ height: config.height || 500 }}
>
<div className="text-center text-muted-foreground">
<Calendar className="h-10 w-10 mx-auto mb-3 opacity-50" />
<p className="text-sm font-medium"> </p>
<p className="text-xs mt-2 max-w-[200px]">
<Calendar className="mx-auto mb-2 h-8 w-8 opacity-50 sm:mb-3 sm:h-10 sm:w-10" />
<p className="text-xs font-medium sm:text-sm"> </p>
<p className="mt-1.5 max-w-[200px] text-[10px] sm:mt-2 sm:text-xs">
,<br />
</p>
@ -292,7 +292,7 @@ export function TimelineSchedulerComponent({
return (
<div
ref={containerRef}
className="w-full border rounded-lg overflow-hidden bg-background"
className="w-full overflow-hidden rounded-lg border bg-background"
style={{
height: config.height || 500,
maxHeight: config.maxHeight,
@ -300,24 +300,24 @@ export function TimelineSchedulerComponent({
>
{/* 툴바 */}
{config.showToolbar !== false && (
<div className="flex items-center justify-between px-3 py-2 border-b bg-muted/30">
<div className="flex items-center justify-between border-b bg-muted/30 px-2 py-1.5 sm:px-3 sm:py-2">
{/* 네비게이션 */}
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5 sm:gap-1">
{config.showNavigation !== false && (
<>
<Button
variant="ghost"
size="sm"
onClick={goToPrevious}
className="h-7 px-2"
className="h-6 px-1.5 sm:h-7 sm:px-2"
>
<ChevronLeft className="h-4 w-4" />
<ChevronLeft className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={goToToday}
className="h-7 px-2"
className="h-6 px-1.5 text-xs sm:h-7 sm:px-2 sm:text-sm"
>
</Button>
@ -325,15 +325,15 @@ export function TimelineSchedulerComponent({
variant="ghost"
size="sm"
onClick={goToNext}
className="h-7 px-2"
className="h-6 px-1.5 sm:h-7 sm:px-2"
>
<ChevronRight className="h-4 w-4" />
<ChevronRight className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
</>
)}
{/* 현재 날짜 범위 표시 */}
<span className="ml-2 text-sm text-muted-foreground">
<span className="ml-1 text-[10px] text-muted-foreground sm:ml-2 sm:text-sm">
{viewStartDate.getFullYear()} {viewStartDate.getMonth() + 1}{" "}
{viewStartDate.getDate()} ~{" "}
{viewEndDate.getMonth() + 1} {viewEndDate.getDate()}
@ -341,20 +341,20 @@ export function TimelineSchedulerComponent({
</div>
{/* 오른쪽 컨트롤 */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 sm:gap-2">
{/* 줌 컨트롤 */}
{config.showZoomControls !== false && (
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5 sm:gap-1">
<Button
variant="ghost"
size="sm"
onClick={handleZoomOut}
disabled={zoomLevel === "month"}
className="h-7 px-2"
className="h-6 px-1.5 sm:h-7 sm:px-2"
>
<ZoomOut className="h-4 w-4" />
<ZoomOut className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
<span className="text-xs text-muted-foreground min-w-[24px] text-center">
<span className="min-w-[20px] text-center text-[10px] text-muted-foreground sm:min-w-[24px] sm:text-xs">
{zoomLevelOptions.find((o) => o.value === zoomLevel)?.label}
</span>
<Button
@ -362,9 +362,9 @@ export function TimelineSchedulerComponent({
size="sm"
onClick={handleZoomIn}
disabled={zoomLevel === "day"}
className="h-7 px-2"
className="h-6 px-1.5 sm:h-7 sm:px-2"
>
<ZoomIn className="h-4 w-4" />
<ZoomIn className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
</div>
)}
@ -375,9 +375,9 @@ export function TimelineSchedulerComponent({
variant="default"
size="sm"
onClick={handleAddClick}
className="h-7"
className="h-6 text-xs sm:h-7 sm:text-sm"
>
<Plus className="h-4 w-4 mr-1" />
<Plus className="mr-0.5 h-3.5 w-3.5 sm:mr-1 sm:h-4 sm:w-4" />
</Button>
)}

View File

@ -138,13 +138,13 @@ export function ResourceRow({
>
{/* 리소스 컬럼 */}
<div
className="flex-shrink-0 border-r bg-muted/30 flex items-center px-3 sticky left-0 z-10"
className="sticky left-0 z-10 flex shrink-0 items-center border-r bg-muted/30 px-2 sm:px-3"
style={{ width: resourceColumnWidth }}
>
<div className="truncate">
<div className="font-medium text-sm truncate">{resource.name}</div>
<div className="truncate text-[10px] font-medium sm:text-sm">{resource.name}</div>
{resource.group && (
<div className="text-xs text-muted-foreground truncate">
<div className="truncate text-[9px] text-muted-foreground sm:text-xs">
{resource.group}
</div>
)}
@ -170,7 +170,7 @@ export function ResourceRow({
<div
key={idx}
className={cn(
"border-r h-full",
"h-full border-r",
isWeekend && "bg-muted/20",
isToday && "bg-primary/5",
isMonthStart && "border-l-2 border-l-primary/20"

View File

@ -125,9 +125,9 @@ export function ScheduleBar({
<div
ref={barRef}
className={cn(
"absolute rounded-md shadow-sm cursor-pointer transition-shadow",
"hover:shadow-md hover:z-10",
isDragging && "opacity-70 shadow-lg z-20",
"absolute cursor-pointer rounded-md shadow-sm transition-shadow",
"hover:z-10 hover:shadow-md",
isDragging && "z-20 opacity-70 shadow-lg",
isResizing && "z-20",
draggable && "cursor-grab",
isDragging && "cursor-grabbing"
@ -145,19 +145,19 @@ export function ScheduleBar({
{/* 진행률 바 */}
{config.showProgress && schedule.progress !== undefined && (
<div
className="absolute inset-y-0 left-0 rounded-l-md opacity-30 bg-white"
className="absolute inset-y-0 left-0 rounded-l-md bg-white opacity-30"
style={{ width: progressWidth }}
/>
)}
{/* 제목 */}
<div className="relative z-10 px-2 py-1 text-xs text-white truncate font-medium">
<div className="relative z-10 truncate px-1.5 py-0.5 text-[10px] font-medium text-white sm:px-2 sm:py-1 sm:text-xs">
{schedule.title}
</div>
{/* 진행률 텍스트 */}
{config.showProgress && schedule.progress !== undefined && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-white/80 font-medium">
<div className="absolute right-1 top-1/2 -translate-y-1/2 text-[8px] font-medium text-white/80 sm:right-2 sm:text-[10px]">
{schedule.progress}%
</div>
)}
@ -165,7 +165,7 @@ export function ScheduleBar({
{/* 리사이즈 핸들 - 왼쪽 */}
{resizable && (
<div
className="absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize hover:bg-white/20 rounded-l-md"
className="absolute bottom-0 left-0 top-0 w-1.5 cursor-ew-resize rounded-l-md hover:bg-white/20 sm:w-2"
onMouseDown={(e) => handleResizeStart("start", e)}
/>
)}
@ -173,7 +173,7 @@ export function ScheduleBar({
{/* 리사이즈 핸들 - 오른쪽 */}
{resizable && (
<div
className="absolute right-0 top-0 bottom-0 w-2 cursor-ew-resize hover:bg-white/20 rounded-r-md"
className="absolute bottom-0 right-0 top-0 w-1.5 cursor-ew-resize rounded-r-md hover:bg-white/20 sm:w-2"
onMouseDown={(e) => handleResizeStart("end", e)}
/>
)}

View File

@ -140,7 +140,7 @@ export function TimelineHeader({
<div className="flex" style={{ height: headerHeight / 2 }}>
{/* 리소스 컬럼 헤더 */}
<div
className="flex-shrink-0 border-r bg-muted/50 flex items-center justify-center font-medium text-sm"
className="flex shrink-0 items-center justify-center border-r bg-muted/50 text-[10px] font-medium sm:text-sm"
style={{ width: resourceColumnWidth }}
>
@ -150,7 +150,7 @@ export function TimelineHeader({
{monthGroups.map((group, idx) => (
<div
key={`${group.year}-${group.month}-${idx}`}
className="border-r flex items-center justify-center text-xs font-medium text-muted-foreground"
className="flex items-center justify-center border-r text-[10px] font-medium text-muted-foreground sm:text-xs"
style={{ width: group.count * cellWidth }}
>
{group.year} {group.month}
@ -162,7 +162,7 @@ export function TimelineHeader({
<div className="flex" style={{ height: headerHeight / 2 }}>
{/* 리소스 컬럼 (빈칸) */}
<div
className="flex-shrink-0 border-r bg-muted/50"
className="shrink-0 border-r bg-muted/50"
style={{ width: resourceColumnWidth }}
/>
@ -171,7 +171,7 @@ export function TimelineHeader({
<div
key={idx}
className={cn(
"border-r flex items-center justify-center text-xs",
"flex items-center justify-center border-r text-[10px] sm:text-xs",
cell.isToday && "bg-primary/10 font-bold text-primary",
cell.isWeekend && !cell.isToday && "bg-muted/30 text-muted-foreground",
cell.isMonthStart && "border-l-2 border-l-primary/30"
@ -186,7 +186,7 @@ export function TimelineHeader({
{/* 오늘 표시선 */}
{showTodayLine && todayPosition !== null && (
<div
className="absolute top-0 bottom-0 w-0.5 bg-primary z-30 pointer-events-none"
className="pointer-events-none absolute bottom-0 top-0 z-30 w-0.5 bg-primary"
style={{ left: todayPosition }}
/>
)}