feat: 메뉴 재배치 및 UX 개선

- 메뉴: 즐겨찾기를 할일 좌측으로 이동
- 게시판: 답글 있는 게시글 삭제 방지 (댓글은 허용)
- 즐겨찾기: ESC 키로 다이얼로그 닫기 지원
- 프로젝트: 기본 필터를 검토/진행/완료로 변경 (보류 해제)
This commit is contained in:
backuppc
2025-12-03 10:32:10 +09:00
parent c1c615fe1b
commit 8e8d1f91b4
6 changed files with 122 additions and 49 deletions

View File

@@ -301,6 +301,19 @@ namespace Project.Web
return JsonConvert.SerializeObject(new { Success = false, Message = "삭제 권한이 없습니다." }); return JsonConvert.SerializeObject(new { Success = false, Message = "삭제 권한이 없습니다." });
} }
// 답글 존재 여부 확인 (is_comment=false인 답글만)
var replyCheckCmd = new SqlCommand(@"
SELECT COUNT(*)
FROM EETGW_Board
WHERE root_idx = @idx AND depth > 0 AND (is_comment IS NULL OR is_comment = 0)", conn);
replyCheckCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
var replyCount = (int)replyCheckCmd.ExecuteScalar();
if (replyCount > 0)
{
return JsonConvert.SerializeObject(new { Success = false, Message = "답글이 있는 게시글은 삭제할 수 없습니다. 답글을 먼저 삭제해주세요." });
}
var cmd = new SqlCommand("DELETE FROM EETGW_Board WHERE idx = @idx", conn); var cmd = new SqlCommand("DELETE FROM EETGW_Board WHERE idx = @idx", conn);
cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;

View File

@@ -93,6 +93,19 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
} }
}, [isOpen]); }, [isOpen]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
if (isOpen) {
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}
}, [isOpen, onClose]);
const loadFavorites = async () => { const loadFavorites = async () => {
setLoading(true); setLoading(true);
try { try {

View File

@@ -80,13 +80,12 @@ const leftDropdownMenus: DropdownMenuConfig[] = [
}, },
]; ];
// 좌측 단독 액션 버튼 (즐겨찾기) // 좌측 단독 액션 버튼
const leftActionItems: NavItem[] = [ const leftActionItems: NavItem[] = [];
{ icon: Star, label: '즐겨찾기', action: 'favorite' },
];
// 우측 메뉴 항목 // 우측 메뉴 항목
const rightNavItems: NavItem[] = [ const rightNavItems: NavItem[] = [
{ icon: Star, label: '즐겨찾기', action: 'favorite' },
{ path: '/todo', icon: CheckSquare, label: '할일' }, { path: '/todo', icon: CheckSquare, label: '할일' },
]; ];
@@ -490,23 +489,34 @@ export function Header(_props: HeaderProps) {
<DropdownNavMenu key={menu.label} menu={menu} onAction={handleAction} /> <DropdownNavMenu key={menu.label} menu={menu} onAction={handleAction} />
))} ))}
{/* 우측 메뉴들 (할일) */} {/* 우측 메뉴들 (즐겨찾기, 할일) */}
{rightNavItems.map((item) => ( {rightNavItems.map((item) => (
<NavLink item.path ? (
key={item.path} <NavLink
to={item.path!} key={item.path}
className={({ isActive }) => to={item.path}
clsx( className={({ isActive }) =>
'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium', clsx(
isActive 'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium',
? 'bg-white/20 text-white shadow-lg' isActive
: 'text-white/70 hover:bg-white/10 hover:text-white' ? 'bg-white/20 text-white shadow-lg'
) : 'text-white/70 hover:bg-white/10 hover:text-white'
} )
> }
<item.icon className="w-4 h-4" /> >
<span>{item.label}</span> <item.icon className="w-4 h-4" />
</NavLink> <span>{item.label}</span>
</NavLink>
) : (
<button
key={item.label}
onClick={() => item.action && handleAction(item.action)}
className="flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-medium text-white/70 hover:bg-white/10 hover:text-white"
>
<item.icon className="w-4 h-4" />
<span>{item.label}</span>
</button>
)
))} ))}
</nav> </nav>
</div> </div>
@@ -573,24 +583,38 @@ export function Header(_props: HeaderProps) {
/> />
))} ))}
{/* 우측 메뉴들 (할일) */} {/* 우측 메뉴들 (즐겨찾기, 할일) */}
{rightNavItems.map((item) => ( {rightNavItems.map((item) => (
<NavLink item.path ? (
key={item.path} <NavLink
to={item.path!} key={item.path}
onClick={() => setIsMobileMenuOpen(false)} to={item.path}
className={({ isActive }) => onClick={() => setIsMobileMenuOpen(false)}
clsx( className={({ isActive }) =>
'flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200', clsx(
isActive 'flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200',
? 'bg-white/20 text-white' isActive
: 'text-white/70 hover:bg-white/10 hover:text-white' ? 'bg-white/20 text-white'
) : 'text-white/70 hover:bg-white/10 hover:text-white'
} )
> }
<item.icon className="w-5 h-5" /> >
<span className="font-medium">{item.label}</span> <item.icon className="w-5 h-5" />
</NavLink> <span className="font-medium">{item.label}</span>
</NavLink>
) : (
<button
key={item.label}
onClick={() => {
if (item.action) handleAction(item.action);
setIsMobileMenuOpen(false);
}}
className="flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200 text-white/70 hover:bg-white/10 hover:text-white w-full text-left"
>
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.label}</span>
</button>
)
))} ))}
</nav> </nav>
</div> </div>

View File

@@ -575,13 +575,36 @@ export function BoardList({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{(userLevel >= 9 || userId === '395552') && ( {(userLevel >= 9 || userId === '395552') && (
<button <>
onClick={handleEditClick} <button
className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors flex items-center" onClick={handleEditClick}
> className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors flex items-center"
<Edit3 className="w-4 h-4 mr-2" /> >
<Edit3 className="w-4 h-4 mr-2" />
</button>
</button>
<button
onClick={async () => {
if (!selectedItem) return;
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const response = await comms.deleteBoard(selectedItem.idx);
if (response.Success) {
setShowModal(false);
loadData();
} else {
alert(response.Message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 오류:', error);
alert('삭제 중 오류가 발생했습니다.');
}
}}
className="px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600 text-white transition-colors"
>
</button>
</>
)} )}
<button <button
onClick={() => setShowModal(false)} onClick={() => setShowModal(false)}

View File

@@ -620,11 +620,11 @@ export function Jobreport() {
</span> </span>
</td> </td>
<td className="px-4 py-3 text-white text-sm"> <td className="px-4 py-3 text-white text-sm">
{item.hrs || 0}h {item.hrs || 0}
</td> </td>
{canViewOT && ( {canViewOT && (
<td className="px-4 py-3 text-white text-sm"> <td className="px-4 py-3 text-white text-sm">
{item.ot ? <span className="text-warning-400">{item.ot}h</span> : '-'} {item.ot ? <span className="text-warning-400">{item.ot}</span> : '-'}
</td> </td>
)} )}
<td className="px-4 py-3 text-white text-sm">{item.name || item.id || '-'}</td> <td className="px-4 py-3 text-white text-sm">{item.name || item.id || '-'}</td>

View File

@@ -42,9 +42,9 @@ export function Project() {
const [statusChecks, setStatusChecks] = useState({ const [statusChecks, setStatusChecks] = useState({
검토: true, 검토: true,
진행: true, 진행: true,
대기: true, 대기: false,
보류: true, 보류: false,
완료: false, 완료: true,
'완료(보고)': false, '완료(보고)': false,
취소: false, 취소: false,
}); });