Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5e437aa
fix: ClubTag 컴포넌트의 중복 key 문제 해결
seongwon030 May 7, 2025
767f743
ix: 모바일 환경에서 배너 슬라이더 초기 렌더링 문제 해결
seongwon030 May 7, 2025
200b0d5
feat: 페이지 체류시간 추적 로직 개선
seongwon030 May 12, 2025
c9a5873
feat: clubName은 매개변수로 전달
seongwon030 May 12, 2025
eefb86c
Merge pull request #381 from Moadong/fix/#380-mobile-banner-initial-r…
seongwon030 May 12, 2025
f1d9824
refactor: 관리자 페이지 탭별 컴포넌트 분리 및 import 경로 수정
oesnuj May 12, 2025
2098f72
feat: 동아리 SNS 링크 입력 영역 스타일 추가
oesnuj May 13, 2025
634b0a2
feat: SNS 플랫폼 config 및 정규식 검증 기준 정의
oesnuj May 13, 2025
87fc788
feat: ClubDetail 타입에 socialLinks 필드 추가
oesnuj May 13, 2025
b04d91a
feat: InputField 에러 상태 및 메시지 표시 기능 추가
oesnuj May 13, 2025
2562d2f
feat: SNS 링크 유효성 검사 함수 추가
oesnuj May 13, 2025
16095ca
feat: SNS 링크 입력 UI 및 수정 기능 통합
oesnuj May 13, 2025
bafce68
refactor: SNS 플랫폼 설정을 객체 기반 단일 구조로 통합
oesnuj May 13, 2025
802b8f8
refactor: SNS 링크 설정 및 유효성 검사 로직 단일화
oesnuj May 13, 2025
78becb2
refactor: SNS 링크 설정 및 검증 로직을 SNS_CONFIG 하나로 통합
oesnuj May 13, 2025
9e22669
refactor: SNS_CONFIG 파일에 있는 SNSPlatform 타입 선언 제거
oesnuj May 13, 2025
f9b2acd
refactor: SNSPlatform 타입을 constants에서 types로 이동하여 책임 분리
oesnuj May 13, 2025
6f63c5a
refactor: import 순서 정리 및 SNSPlatform 타입 경로 수정
oesnuj May 13, 2025
7da94b3
refactor: SNSPlatform 타입 경로를 constants에서 types로 변경
oesnuj May 13, 2025
f88695e
test: SNS 링크 유효성 검사 테스트 코드 작성
oesnuj May 13, 2025
00c5c28
Merge pull request #398 from Moadong/feature/#397-mixpanel-clubname-a…
seongwon030 May 14, 2025
831790a
Merge pull request #400 from Moadong/feature/#395-admin-club-sns-inpu…
oesnuj May 14, 2025
a2ebcb2
chore: sentry 라이브러리 추가
seongwon030 May 14, 2025
dead7b8
chore: ignore에 sentry env 추가
seongwon030 May 14, 2025
be3513a
feat: sentry 연결 추가
seongwon030 May 14, 2025
58b46db
fix: sendDefaultPii false로 변경
seongwon030 May 14, 2025
9b3b36b
Merge pull request #404 from Moadong/feature/#402-add-sentry-FE-113
seongwon030 May 14, 2025
4dc9330
feat: X(Twitter) 플랫폼용 아이콘 추가
oesnuj May 14, 2025
14e8f6d
feat: YouTube 플랫폼용 아이콘 추가
oesnuj May 14, 2025
2641daf
fix: startTime.current 프로퍼티 추가
seongwon030 May 14, 2025
024349f
fix: 중복 visited 제거
seongwon030 May 14, 2025
972f888
feat: Instagram 플랫폼용 아이콘(png) 추가
oesnuj May 15, 2025
f27a998
refactor: YouTube 플랫폼용 아이콘 비율 변경
oesnuj May 15, 2025
09a16f7
feat: SNS_CONFIG에 플랫폼별 아이콘 이미지 속성 추가
oesnuj May 15, 2025
4fa9f23
feat: SNS 링크 영역 추가를 위해 동아리 정보 섹션 height 조정
oesnuj May 15, 2025
c37acf8
feat: SNS 링크 아이콘 컴포넌트
oesnuj May 15, 2025
b3b6882
feat: SNS 링크 아이콘 컴포넌트
oesnuj May 15, 2025
cb40300
feat: InfoBox 동아리정보 섹션에 SNS 아이콘 추가
oesnuj May 15, 2025
e380a5f
fix: 모바일 환경에서 InfoBox 레이아웃 깨짐 문제 수정
oesnuj May 15, 2025
836c188
refactor: SnsLinkIcons 컴포넌트의 재사용성과 일반성 개선
oesnuj May 15, 2025
9ea3cee
refactor: SnsLinkIcons 컴포넌트 props 네이밍 개선
oesnuj May 15, 2025
3194092
fix: 모바일 환경에서 InfoBox의 gap 속성 수정
oesnuj May 15, 2025
c456740
feat: form스타일 컴포넌트 추가
seongwon030 May 15, 2025
b00a18c
feat: onClick 선택적 프로퍼티로 설정
seongwon030 May 15, 2025
a45168e
feat: type 프로퍼티 추가
seongwon030 May 15, 2025
4bf5e2c
feat: 로그인박스 form태그로 감싸기
seongwon030 May 15, 2025
a34ebfc
feat: 버튼 type속성 컴포넌트에 전달
seongwon030 May 15, 2025
3540bca
Merge pull request #408 from Moadong/fix/#407-useref-start-time-curre…
seongwon030 May 15, 2025
c914817
Merge pull request #379 from Moadong/fix/#378-club-tag-duplicate-key-…
seongwon030 May 15, 2025
8c30dce
Merge pull request #411 from Moadong/feature/#410-adminPage-login-ent…
seongwon030 May 15, 2025
0cff80b
Merge pull request #409 from Moadong/feature/#403-club-detail-sns-dis…
oesnuj May 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ build-storybook.log
*.csr

*storybook.log
coverage/
coverage/
# Sentry Config File
.env.sentry-build-plugin
584 changes: 527 additions & 57 deletions frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"description": "",
"dependencies": {
"@channel.io/channel-web-sdk-loader": "^2.0.0",
"@sentry/react": "^9.18.0",
"@sentry/webpack-plugin": "^3.4.0",
"@tanstack/react-query": "^5.66.0",
"date-fns": "^4.1.0",
"dotenv-webpack": "^8.1.0",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/images/icons/sns/x_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/images/icons/sns/youtube_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions frontend/src/components/common/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import styled, { keyframes, css } from 'styled-components';
export interface ButtonProps {
width?: string;
children: React.ReactNode;
onClick: () => void;
type?: string;
onClick?: () => void;
animated?: boolean;
}

Expand Down Expand Up @@ -44,9 +45,10 @@ const Button = ({
width,
children,
onClick,
type,
animated = false,
}: ButtonProps) => (
<StyledButton width={width} onClick={onClick} animated={animated}>
<StyledButton width={width} onClick={onClick} animated={animated} type={type}>
{children}
</StyledButton>
);
Expand Down
27 changes: 22 additions & 5 deletions frontend/src/components/common/InputField/InputField.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,32 @@ export const InputWrapper = styled.div`
align-items: center;
`;

export const Input = styled.input`
export const Input = styled.input<{ hasError?: boolean }>`
flex: 1;
height: 45px;
padding: 12px 80px 12px 18px;
border: 1px solid #c5c5c5;
border: 1px solid ${({ hasError }) => (hasError ? 'red' : '#c5c5c5')};
border-radius: 6px;
outline: none;
font-size: 1.125rem;
letter-spacing: 0;
color: rgba(0, 0, 0, 0.5);
color: rgba(0, 0, 0, 0.8);

&:focus {
border-color: #007bff;
box-shadow: 0 0 3px rgba(0, 123, 255, 0.5);
border-color: ${({ hasError }) => (hasError ? 'red' : '#007bff')};
box-shadow: 0 0 3px
${({ hasError }) =>
hasError ? 'rgba(255, 0, 0, 0.5)' : 'rgba(0, 123, 255, 0.5)'};
}

${({ disabled }) =>
disabled &&
`
background-color: rgba(0, 0, 0, 0.05);
`}
&::placeholder {
color: rgba(0, 0, 0, 0.3);
}
`;

export const ClearButton = styled.button`
Expand Down Expand Up @@ -85,3 +90,15 @@ export const CharCount = styled.span`
font-size: 12px;
letter-spacing: -0.96px;
`;

export const HelperText = styled.div`
position: absolute;
left: 0;
top: 100%;
font-size: 0.75rem;
color: red;
margin-top: 4px;
pointer-events: none;
white-space: nowrap;
z-index: 1;
`;
8 changes: 8 additions & 0 deletions frontend/src/components/common/InputField/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface CustomInputProps {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClear?: () => void;
isError?: boolean;
helperText?: string;
}

const InputField = ({
Expand All @@ -28,6 +30,8 @@ const InputField = ({
value = '',
onChange,
onClear,
isError,
helperText,
}: CustomInputProps) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);

Expand Down Expand Up @@ -64,6 +68,7 @@ const InputField = ({
placeholder={placeholder}
maxLength={maxLength}
disabled={disabled}
hasError={isError}
/>
{showClearButton && !disabled && (
<Styled.ClearButton type='button' onClick={clearInput}>
Expand All @@ -81,6 +86,9 @@ const InputField = ({
</Styled.CharCount>
)}
</Styled.InputWrapper>
{isError && helperText && (
<Styled.HelperText>{helperText}</Styled.HelperText>
)}
</Styled.InputContainer>
);
};
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/constants/snsConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import youtube_icon from '@/assets/images/icons/sns/youtube_icon.svg';
import instagram_icon from '@/assets/images/icons/sns/instagram_icon.png';
import x_icon from '@/assets/images/icons/sns/x_icon.svg';

export const SNS_CONFIG = {
instagram: {
label: '인스타그램',
placeholder: 'https://www.instagram.com/id',
regex: /^https:\/\/(www\.)?instagram\.com\/[A-Za-z0-9._%-]+\/?$/,
icon: instagram_icon,
},
youtube: {
label: '유튜브',
placeholder: 'https://www.youtube.com/@id',
regex: /^https:\/\/(www\.)?youtube\.com\/(channel\/|@)[A-Za-z0-9._%-]+\/?$/,
icon: youtube_icon,
},
x: {
label: 'X',
placeholder: 'https://x.com/id',
regex: /^https:\/\/(www\.)?x\.com\/[A-Za-z0-9._%-]+\/?$/,
icon: x_icon,
},
} as const;
27 changes: 14 additions & 13 deletions frontend/src/hooks/useTrackPageView.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import mixpanel from 'mixpanel-browser';

const useTrackPageView = (pageName: string) => {
const useTrackPageView = (pageName: string, clubName?: string) => {
const location = useLocation();
const isTracked = useRef(false);
const startTime = useRef(Date.now());

useEffect(() => {
const startTime = Date.now();

// 페이지 방문 이벤트
mixpanel.track(`${pageName} Visited`, {
url: window.location.href,
timestamp: startTime,
timestamp: startTime.current,
referrer: document.referrer || 'direct',
clubName,
});

const trackPageDuration = () => {
const duration = Date.now() - startTime;
if (isTracked.current) return;
const duration = Date.now() - startTime.current;
mixpanel.track(`${pageName} Duration`, {
url: window.location.href,
duration: duration, // milliseconds
duration_seconds: Math.round(duration / 1000), // Convert to seconds
duration: duration,
duration_seconds: Math.round(duration / 1000),
clubName,
});
isTracked.current = true;
};

// 사용자가 페이지를 떠날 때 (페이지 종료 또는 새 페이지 이동)
window.addEventListener('beforeunload', trackPageDuration);

// 사용자가 탭을 변경하거나 백그라운드로 이동할 때
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
trackPageDuration();
}
});

return () => {
trackPageDuration();
window.removeEventListener('beforeunload', trackPageDuration);
document.removeEventListener('visibilitychange', trackPageDuration);
};
}, [location.pathname]);
}, [location.pathname, clubName]);
};

export default useTrackPageView;
8 changes: 8 additions & 0 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import mixpanel from 'mixpanel-browser';
import * as ChannelService from '@channel.io/channel-web-sdk-loader';
import * as Sentry from '@sentry/react';

if (process.env.REACT_APP_MIXPANEL_TOKEN) {
mixpanel.init(process.env.REACT_APP_MIXPANEL_TOKEN, {
Expand All @@ -22,6 +23,13 @@ if (process.env.CHANNEL_PLUGIN_KEY) {
});
}

Sentry.init({
dsn: process.env.SENTRY_DSN,
sendDefaultPii: false,
release: process.env.SENTRY_RELEASE,
tracesSampleRate: 0.1,
});

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const Title = styled.h2`
align-self: flex-start;
`;

export const LoginForm = styled.form`
width: 100%;
display: flex;
flex-direction: column;
`;

export const InputFieldsContainer = styled.div`
width: 100%;
display: flex;
Expand Down
58 changes: 34 additions & 24 deletions frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,35 @@ const LoginTab = () => {
<Styled.LoginBox>
<Styled.Logo src={moadong_name_logo} alt='Moadong Logo' />
<Styled.Title>Log in</Styled.Title>
<Styled.InputFieldsContainer>
<InputField
type='text'
placeholder='아이디'
showClearButton={false}
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<InputField
type='password'
placeholder='비밀번호'
showClearButton={false}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Styled.InputFieldsContainer>
<Styled.LoginForm
onSubmit={(e) => {
e.preventDefault();
handleLogin();
}}
>
<Styled.InputFieldsContainer>
<InputField
type='text'
placeholder='아이디'
showClearButton={false}
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<InputField
type='password'
placeholder='비밀번호'
showClearButton={false}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Styled.InputFieldsContainer>

<Styled.ButtonWrapper>
<Button width='100%' onClick={handleLogin}>
{loading ? '로그인 중...' : '로그인'}
</Button>
</Styled.ButtonWrapper>
<Styled.ButtonWrapper>
<Button width='100%' type='submit'>
{loading ? '로그인 중...' : '로그인'}
</Button>
</Styled.ButtonWrapper>
</Styled.LoginForm>

<Styled.ForgotLinks>
<Styled.LinkButton
Expand All @@ -79,7 +86,8 @@ const LoginTab = () => {
alert(
'해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺',
)
}>
}
>
회원가입
</Styled.LinkButton>
<span>|</span>
Expand All @@ -89,7 +97,8 @@ const LoginTab = () => {
alert(
'해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺',
)
}>
}
>
아이디 찾기
</Styled.LinkButton>
<span>|</span>
Expand All @@ -99,7 +108,8 @@ const LoginTab = () => {
alert(
'해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺',
)
}>
}
>
비밀번호 찾기
</Styled.LinkButton>
</Styled.ForgotLinks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,27 @@ export const TagEditGroup = styled.div`
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 120px;
`;

export const SNSInputGroup = styled.div`
display: flex;
flex-direction: column;
gap: 30px;
margin-top: 30px;
`;

export const SNSRow = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
align-items: flex-start;
`;

export const SNSCheckboxLabel = styled.label`
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
`;
Loading