Skip to content

feat : WEB-9 관리자 메인 페이지#22

Open
PJW03 wants to merge 4 commits into
developfrom
feature-WEB-9
Open

feat : WEB-9 관리자 메인 페이지#22
PJW03 wants to merge 4 commits into
developfrom
feature-WEB-9

Conversation

@PJW03

@PJW03 PJW03 commented Jul 4, 2026

Copy link
Copy Markdown

📌 작업 내용

관리자 메인 페이지 퍼블리싱

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the admin home page to reuse visitor sections (ActivitiesSection, TimelineSection, and RecruitSection) with inline editing capabilities powered by a new Zustand store (useHomeContentStore). The review feedback identifies several critical issues and improvement opportunities: a regression in TimelineSection where the expand/collapse detailed view was omitted, a bug preventing saves when the tech stack is empty, a limitation with saving partial project periods, a potential memory leak from unrevoked object URLs, a potential runtime error in highlightOne due to missing null checks, and opportunities to optimize state updates by batching recruitment content updates.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +287 to 309
function TimelineCard({ event, isRight, isEditing, formRef, onSaveEdit, onCancelEdit }) {
return (
<div className={`relative bg-white rounded-2xl px-9 py-6 shadow-md ${isRight ? 'text-left' : 'text-right'}`}>
<span
aria-hidden="true"
className={`absolute top-6 h-3 w-3 rotate-45 bg-white ${isRight ? '-left-1.5' : '-right-1.5'}`}
/>

{isEditing ? (
<TimelineEditForm ref={formRef} event={event} onSave={onSaveEdit} onCancel={onCancelEdit} />
) : (
<div className={isRight ? '' : 'ml-auto'}>
<p className="text-sm text-brand font-bold m-0 mb-2">
{event.year}
{event.projectName ? `, ${event.projectName}` : ''}
</p>
<p className="text-base font-bold text-ink m-0 mb-2">{event.award}</p>
<p className="text-xs text-[#AAAAAA] m-0">{event.activity}</p>
</div>
)}
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

기존에 구현되어 있던 타임라인 카드의 상세 정보(기간, 인원, 기술 스택, 프로젝트 소개, 이미지 등)를 보여주는 펼치기/접기(Expand/Collapse) 기능과 상세 렌더링 로직이 완전히 누락되었습니다.

이로 인해 방문자 페이지에서 프로젝트의 상세 정보를 확인할 수 없는 심각한 기능 퇴화(Regression)가 발생합니다.

수정 모드가 아닐 때는 기존처럼 클릭 시 상세 내용이 펼쳐지도록 로컬 상태(isExpanded)를 추가하고 상세 렌더링 로직을 복구해야 합니다.

function TimelineCard({ event, isRight, isEditing, formRef, onSaveEdit, onCancelEdit }) {
  const [isExpanded, setIsExpanded] = useState(false);

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      setIsExpanded((prev) => !prev);
    }
  };

  return (
    <div
      onClick={() => !isEditing && setIsExpanded((prev) => !prev)}
      onKeyDown={handleKeyDown}
      role={isEditing ? undefined : 'button'}
      tabIndex={isEditing ? undefined : 0}
      aria-expanded={isExpanded}
      className={`relative bg-white rounded-2xl px-9 py-6 shadow-md transition-all duration-300 ${
        isExpanded ? 'border-brand' : 'border-[#EDCFBC]'
      } ${isRight ? 'text-left' : 'text-right'} ${isEditing ? '' : 'cursor-pointer select-none'}`}
    >
      <span
        aria-hidden="true"
        className={`absolute top-6 h-3 w-3 rotate-45 bg-white ${isRight ? '-left-1.5' : '-right-1.5'}`}
      />

      {isEditing ? (
        <TimelineEditForm ref={formRef} event={event} onSave={onSaveEdit} onCancel={onCancelEdit} />
      ) : (
        <>
          <div className={isRight ? '' : 'ml-auto'}>
            <p className="text-sm text-brand font-bold m-0 mb-2">
              {event.year}
              {event.projectName ? `, ${event.projectName}` : ''}
            </p>
            <p className="text-base font-bold text-ink m-0 mb-2">{event.award}</p>
            <p className="text-xs text-[#AAAAAA] m-0">{event.activity}</p>
          </div>

          <div
            className={`overflow-hidden transition-all duration-400 ease-in-out ${
              isExpanded ? 'max-h-[600px] opacity-100 mt-4' : 'max-h-0 opacity-0'
            }`}
          >
            <hr className="border-[#EDCFBC] mb-4" />

            <div className={`flex gap-6 mb-4 text-xs text-ink-sub ${isRight ? '' : 'justify-end'}`}>
              <span className="flex items-center gap-1">
                <span>📅</span> {event.period}
              </span>
              {event.memberCount && (
                <span className="flex items-center gap-1">
                  <span>👥</span> {event.memberCount}</span>
              )}
            </div>

            {event.techStack && event.techStack.length > 0 && (
              <div className={`mb-4 ${isRight ? '' : 'flex flex-col items-end'}`}>
                <p className="text-xs text-[#AAAAAA] m-0 mb-2">기술 스택</p>
                <div className={`flex flex-wrap gap-2 ${isRight ? '' : 'justify-end'}`}>
                  {event.techStack.map((tech) => (
                    <span
                      key={tech}
                      className="px-3 py-1 rounded-full bg-brand-soft text-brand text-xs font-medium"
                    >
                      {tech}
                    </span>
                  ))}
                </div>
              </div>
            )}

            <div className={`mb-4 ${isRight ? '' : 'text-right'}`}>
              <p className="text-xs text-[#AAAAAA] m-0 mb-2">프로젝트 소개</p>
              <p className="text-sm text-ink leading-6 m-0 whitespace-pre-line">{event.description}</p>
            </div>

            {event.images && event.images.length > 0 && (
              <div className="grid grid-cols-3 gap-3 mt-2">
                {event.images.map((src, index) => (
                  <img
                    key={index}
                    src={src}
                    alt={`${event.projectName} 이미지 ${index + 1}`}
                    className="w-full aspect-[4/3] object-cover rounded-lg bg-[#F0F0F0]"
                  />
                ))}
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영완료

Comment on lines +90 to +94
const validTechStack = techStack.map((tech) => tech.trim()).filter(Boolean);
if (validTechStack.length === 0) {
alert('기술 스택을 입력해주세요.');
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

techStack이 비어있는 이벤트(예: 기획 단계인 2026년도 기본 데이터)를 수정하려고 할 때, 기술 스택이 없으면 저장이 불가능한 버그가 있습니다.

EMPTY_EVENT나 2026년도 데이터처럼 기술 스택이 아직 정의되지 않은 상태에서도 저장이 가능하도록 필수 입력 검증 조건을 완화하거나 제거해야 합니다.

Suggested change
const validTechStack = techStack.map((tech) => tech.trim()).filter(Boolean);
if (validTechStack.length === 0) {
alert('기술 스택을 입력해주세요.');
return;
}
const validTechStack = techStack.map((tech) => tech.trim()).filter(Boolean);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기획서상 내용이없으면 수정이 안되도록 되어있음

Comment on lines +101 to +104
period:
periodStart && periodEnd
? `${periodStart.replace('-', '.')} - ${periodEnd.replace('-', '.')}`
: '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 코드에서는 periodStartperiodEnd둘 다 존재할 때만 period가 저장되고, 하나라도 비어있으면 빈 문자열('')로 초기화됩니다.

진행 중인 프로젝트의 경우 종료일(periodEnd)을 비워둘 수 있으므로, 시작일만 있거나 종료일만 있는 경우에도 정상적으로 저장되도록 개선하는 것이 좋습니다.

Suggested change
period:
periodStart && periodEnd
? `${periodStart.replace('-', '.')} - ${periodEnd.replace('-', '.')}`
: '',
period: [periodStart, periodEnd]
.filter(Boolean)
.map((p) => p.replace('-', '.'))
.join(' - '),

Comment on lines +80 to +82
const handleRemoveImage = (index) => {
setImages((previous) => previous.filter((_, i) => i !== index));
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

URL.createObjectURL로 생성된 객체 URL은 브라우저 메모리에 계속 유지되므로, 이미지를 삭제할 때 URL.revokeObjectURL을 호출하여 메모리 누수를 방지하는 것이 좋습니다.

  const handleRemoveImage = (index) => {
    const targetImage = images[index];
    if (targetImage && targetImage.startsWith('blob:')) {
      URL.revokeObjectURL(targetImage);
    }
    setImages((previous) => previous.filter((_, i) => i !== index));
  };

Comment on lines +10 to +11
function highlightOne(text) {
return text.split('\n').map((line, lineIndex) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

text 매개변수가 null 또는 undefined이거나 문자열이 아닐 경우 split 메서드 호출 시 런타임 에러가 발생할 수 있습니다. 방어적 프로그래밍을 위해 안전 장치를 추가하는 것이 좋습니다.

function highlightOne(text) {
  if (!text) return null;
  return text.split('\n').map((line, lineIndex) => (

Comment thread src/stores/homeContentStore.js Outdated
Comment on lines +37 to +41
updateRecruitHeading: (heading) => set({ recruitHeading: heading }),
updateRecruitInfoItem: (index, value) =>
set((state) => ({
recruitInfoList: state.recruitInfoList.map((item, i) => (i === index ? { ...item, value } : item)),
})),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 RecruitSection에서 저장 시 updateRecruitHeadingupdateRecruitInfoItem을 루프 내에서 각각 호출하고 있어 불필요한 다중 상태 업데이트(State Update)와 리렌더링이 발생합니다.

이를 개선하기 위해 제목과 정보 리스트를 한 번에 업데이트하는 단일 액션(updateRecruitContent)을 정의하여 성능을 최적화하는 것을 권장합니다.

  updateRecruitContent: (heading, values) =>
    set((state) => ({
      recruitHeading: heading,
      recruitInfoList: state.recruitInfoList.map((item, i) => ({
        ...item,
        value: values[i] !== undefined ? values[i] : item.value,
      })),
    })),

Comment on lines +76 to +85
const updateRecruitHeading = useHomeContentStore((state) => state.updateRecruitHeading);
const updateRecruitInfoItem = useHomeContentStore((state) => state.updateRecruitInfoItem);

const [isEditing, setIsEditing] = useState(false);

const handleSave = (heading, values) => {
updateRecruitHeading(heading);
values.forEach((value, index) => updateRecruitInfoItem(index, value));
setIsEditing(false);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

homeContentStore에 제안된 단일 업데이트 액션(updateRecruitContent)을 사용하여 불필요한 다중 렌더링을 방지하고 코드를 간결하게 유지합니다.

  const updateRecruitContent = useHomeContentStore((state) => state.updateRecruitContent);

  const [isEditing, setIsEditing] = useState(false);

  const handleSave = (heading, values) => {
    updateRecruitContent(heading, values);
    setIsEditing(false);
  };

@takjinwu

takjinwu commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

활동 현황 값 수정 후에 저장 클릭 시에 실제로 수정한 값들이 적용되게 만들어주세요. 현재는 활동 현황이 수정되지 않고 사라집니다.
또한 삭제 시에 모든 활동 현황이 사라지는데, 선택한 활동 현황만 사라지도록 해주세요.

@takjinwu

takjinwu commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

활동 현황 수정 시에 팀원이 기간에 겹쳐 기간 선택이 불가합니다. 수정해주세요

@takjinwu

takjinwu commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

현재 글자수 제한이 없습니다. 글자수 제한 적당히 주세요 과도한 값을 백엔드 서버에 전송 시 백엔드 서버가 터질 위험성이 있습니다

@takjinwu takjinwu closed this Jul 4, 2026
@takjinwu takjinwu reopened this Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants