Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@
"음소거": "Mute",
"음소거 해제": "Unmute",
"발언자는 최대 {{MAX_SPEAKER_LEN}}자까지 입력할 수 있습니다.": "Speaker can be up to {{MAX_SPEAKER_LEN}} characters.",
"템플릿을 불러오는 중입니다...": "Loading templates...",
"템플릿을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.": "Failed to load templates. Please try again later.",
"400 잘못된 요청": "400 Bad Request",
"401 권한 없음": "401 Unauthorized",
"403 거부됨": "403 Forbidden",
Expand Down
4 changes: 3 additions & 1 deletion public/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"이전 차례": "이전 차례",
"다음 차례": "다음 차례",
"토론 종료": "토론 종료",
"알림 개수_one": "알림 {{displayCount}}개",
"알림 개수_one": "알림 {{displayCount}}개",
"알림 개수_other": "알림 {{displayCount}}개",
"{{team}} 팀": "{{team}} 팀",
"데이터를 불러오고 있습니다...": "데이터를 불러오고 있습니다...",
Expand Down Expand Up @@ -241,6 +241,8 @@
"음소거": "음소거",
"음소거 해제": "음소거 해제",
"발언자는 최대 {{MAX_SPEAKER_LEN}}자까지 입력할 수 있습니다.": "발언자는 최대 {{MAX_SPEAKER_LEN}}자까지 입력할 수 있습니다.",
"템플릿을 불러오는 중입니다...": "템플릿을 불러오는 중입니다...",
"템플릿을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.": "템플릿을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.",
"400 잘못된 요청": "400 잘못된 요청",
"401 권한 없음": "401 권한 없음",
"403 거부됨": "403 거부됨",
Expand Down
16 changes: 16 additions & 0 deletions src/apis/apis/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ApiUrl } from '../endpoints';
import { request } from '../primitives';
import { GetOrganizationTemplatesResponseType } from '../responses/organization';

// GET /api/organizations/templates
export async function getOrganizationTemplates(): Promise<GetOrganizationTemplatesResponseType> {
const requestUrl: string = ApiUrl.organization + '/templates';
const response = await request<GetOrganizationTemplatesResponseType>(
'GET',
requestUrl,
null,
null,
);

return response.data;
}
1 change: 1 addition & 0 deletions src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const ApiUrl = {
parliamentary: makeUrl('/table/parliamentary'),
customize: makeUrl('/table/customize'),
poll: makeUrl('/polls'),
organization: makeUrl('/organizations'),
};
5 changes: 5 additions & 0 deletions src/apis/responses/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Organization } from '../../type/type';

export interface GetOrganizationTemplatesResponseType {
organizations: Organization[];
}
Binary file removed src/assets/template_logo/han_alm.png
Binary file not shown.
Binary file removed src/assets/template_logo/hantomak.png
Binary file not shown.
Binary file removed src/assets/template_logo/igam.png
Binary file not shown.
Binary file removed src/assets/template_logo/jungseonto.png
Binary file not shown.
Binary file removed src/assets/template_logo/kogito.png
Binary file not shown.
Binary file removed src/assets/template_logo/kondae_time.png
Binary file not shown.
Binary file removed src/assets/template_logo/mcu.png
Binary file not shown.
Binary file removed src/assets/template_logo/nogotte.png
Binary file not shown.
Binary file removed src/assets/template_logo/osansi.png
Binary file not shown.
Binary file removed src/assets/template_logo/seobangjeongto.png
Binary file not shown.
Binary file removed src/assets/template_logo/todallae.png
Binary file not shown.
Binary file removed src/assets/template_logo/visual.png
Binary file not shown.
Binary file removed src/assets/template_logo/yuppm_law_track.png
Binary file not shown.
4 changes: 2 additions & 2 deletions src/components/ShareModal/ShareModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/react';
import ShareModal from './ShareModal';
import { createTableShareUrl } from '../../util/arrayEncoding';
import { createTableShareUrlFromTable } from '../../util/arrayEncoding';

const meta: Meta<typeof ShareModal> = {
title: 'components/ShareModal',
Expand All @@ -12,7 +12,7 @@ export default meta;

type Story = StoryObj<typeof ShareModal>;

const shareUrl = createTableShareUrl('https://localhost:6006', {
const shareUrl = createTableShareUrlFromTable('https://localhost:6006', {
info: {
agenda: '토론 주제',
prosTeamName: '짜장',
Expand Down
307 changes: 0 additions & 307 deletions src/constants/debate_template.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/hooks/query/useGetOrganizationTemplates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { GetOrganizationTemplatesResponseType } from '../../apis/responses/organization';
import { getOrganizationTemplates } from '../../apis/apis/organization';

export function useGetOrganizationTemplates(enabled?: boolean) {
return useQuery<GetOrganizationTemplatesResponseType>({
queryKey: ['OrganizationTemplates'],
queryFn: () => getOrganizationTemplates(),
enabled,
throwOnError: false,
});
}
4 changes: 2 additions & 2 deletions src/hooks/useTableShare.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useModal } from './useModal';
import ShareModal from '../components/ShareModal/ShareModal';
import { useGetDebateTableData } from './query/useGetDebateTableData';
import { useEffect, useState } from 'react';
import { createTableShareUrl } from '../util/arrayEncoding';
import { createTableShareUrlFromTable } from '../util/arrayEncoding';

export function useTableShare(tableId: number) {
const { isOpen, openModal, closeModal, ModalWrapper } = useModal();
Expand Down Expand Up @@ -32,7 +32,7 @@ export function useTableShare(tableId: number) {
// Process URL when data is successfully fetched
useEffect(() => {
if (data) {
setShareUrl(createTableShareUrl(baseUrl, data));
setShareUrl(createTableShareUrlFromTable(baseUrl, data));
}
}, [baseUrl, data]);

Expand Down
2 changes: 1 addition & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if (import.meta.env.VITE_MOCK_API === 'true') {
.start({
onUnhandledRequest: (request, print) => {
// Let worker dismiss non-api calls by check whether url includes '/api'
if (!request.url.includes('/api')) {
if (!request.url.includes('/api') && !request.url.includes('/icon')) {
console.log(
"Dismissed request that doesn't include /api/: " + request.url,
);
Expand Down
4 changes: 4 additions & 0 deletions src/mocks/handlers/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { http, HttpResponse } from 'msw';
import { customizeHandlers } from './customize';
import { memberHandlers } from './member';
import { pollHandlers } from './poll';
import { organizationHandlers } from './organization';
import { staticAssetHandlers } from './static_asset';

const TRANSLATIONS: Record<string, Record<string, string>> = {
ko: {
Expand Down Expand Up @@ -45,4 +47,6 @@ export const allHandlers = [
...memberHandlers,
...customizeHandlers,
...pollHandlers,
...organizationHandlers,
...staticAssetHandlers,
];
42 changes: 42 additions & 0 deletions src/mocks/handlers/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { http, HttpResponse } from 'msw';
import { ApiUrl } from '../../apis/endpoints';

export const organizationHandlers = [
// GET /api/organizations/templates
http.get(ApiUrl.organization + '/templates', () => {
return HttpResponse.json({
organizations: [
{
organization: '한앎',
affiliation: '한양대',
iconPath: '/icon/icon1.png',
templates: [
{
name: '템플릿1',
data: 'eJyrVspMUbIytjDXUcrMS8tXsqpWykvMTVWyUjJWKCtWMFZ427b1TXPj27YFrxcueN3T8HZWj8LbGVPfdM9V0lEqqSwAqXQODQ7x9%2FWMcgUKJaan5qUkAgWB7IKi%2FOKQ1MRcP4iBbzasedOyESienJ%2BHLP56wwygwUDx8sSivMy8dKfUnBwlq7TEnOJUHaW0zLzM4gwkoVqgtYlJOUCN0dVKxSWJeckgMwKC%2FIOBJhQXpKYmZ4RAnPVmXivQzUDRpPwKqJCff5Cvow%2FI5Zkgqw2NDICyYLPzSnNyIMIBqUUgx6EJBRekJmYDHQcTLgbxU4sg3FodJKc4%2B%2FsNFqf4uYaGBIEtQXPNhDdzFkCiFMVNIZ6%2BrvFOjsGuLnB3QazA6TBzkLMx3AX2DIkhtHXOm0WtCq83zHkzbQfdAwpr8qG7i2JrAbdLRw0%3D',
},
{
name: '템플릿2',
data: 'eJyrVspMUbIytjDXUcrMS8tXsqpWykvMTVWyUjJWKCtWMFZ427b1TXPj27YFrxcueN3T8HZWj8LbGVPfdM9V0lEqqSwAqXQODQ7x9%2FWMcgUKJaan5qUkAgWB7IKi%2FOKQ1MRcP4iBbzasedOyESienJ%2BHLP56wwygwUDx8sSivMy8dKfUnBwlq7TEnOJUHaW0zLzM4gwkoVqgtYlJOUCN0dVKxSWJeckgMwKC%2FIOBJhQXpKYmZ4RAnPVmXivQzUDRpPwKqJCff5Cvow%2FI5Zkgqw2NDICyYLPzSnNyIMIBqUUgx6EJBRekJmYDHQcTLgbxU4sg3FodJKc4%2B%2FsNFqf4uYaGBIEtQXPNhDdzFkCiFMVNIZ6%2BrvFOjsGuLnB3QazA6TBzkLMx3AX2DIkhtHXOm0WtCq83zHkzbQfdAwpr8qG7i2JrAbdLRw0%3D',
},
],
},
{
organization: '한모름',
affiliation: '양한대',
iconPath: '/icon/icon2.png',
templates: [
{
name: '템플릿1',
data: 'eJyrVspMUbIytjDXUcrMS8tXsqpWykvMTVWyUjJWKCtWMFZ427b1TXPj27YFrxcueN3T8HZWj8LbGVPfdM9V0lEqqSwAqXQODQ7x9%2FWMcgUKJaan5qUkAgWB7IKi%2FOKQ1MRcP4iBbzasedOyESienJ%2BHLP56wwygwUDx8sSivMy8dKfUnBwlq7TEnOJUHaW0zLzM4gwkoVqgtYlJOUCN0dVKxSWJeckgMwKC%2FIOBJhQXpKYmZ4RAnPVmXivQzUDRpPwKqJCff5Cvow%2FI5Zkgqw2NDICyYLPzSnNyIMIBqUUgx6EJBRekJmYDHQcTLgbxU4sg3FodJKc4%2B%2FsNFqf4uYaGBIEtQXPNhDdzFkCiFMVNIZ6%2BrvFOjsGuLnB3QazA6TBzkLMx3AX2DIkhtHXOm0WtCq83zHkzbQfdAwpr8qG7i2JrAbdLRw0%3D',
},
{
name: '템플릿2',
data: 'eJyrVspMUbIytjDXUcrMS8tXsqpWykvMTVWyUjJWKCtWMFZ427b1TXPj27YFrxcueN3T8HZWj8LbGVPfdM9V0lEqqSwAqXQODQ7x9%2FWMcgUKJaan5qUkAgWB7IKi%2FOKQ1MRcP4iBbzasedOyESienJ%2BHLP56wwygwUDx8sSivMy8dKfUnBwlq7TEnOJUHaW0zLzM4gwkoVqgtYlJOUCN0dVKxSWJeckgMwKC%2FIOBJhQXpKYmZ4RAnPVmXivQzUDRpPwKqJCff5Cvow%2FI5Zkgqw2NDICyYLPzSnNyIMIBqUUgx6EJBRekJmYDHQcTLgbxU4sg3FodJKc4%2B%2FsNFqf4uYaGBIEtQXPNhDdzFkCiFMVNIZ6%2BrvFOjsGuLnB3QazA6TBzkLMx3AX2DIkhtHXOm0WtCq83zHkzbQfdAwpr8qG7i2JrAbdLRw0%3D',
},
],
},
],
});
}),
];
31 changes: 31 additions & 0 deletions src/mocks/handlers/static_asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { http, HttpResponse } from 'msw';
import sampleLogo from '../../assets/template_logo/government.png';

const baseUrl = import.meta.env.VITE_API_BASE_URL || '';

export const staticAssetHandlers = [
http.get(baseUrl + '/icon/:iconFileName', async ({ params }) => {
const { iconFileName } = params;
console.log(`# Requested icon file's name = ${iconFileName}`);

const targetLocalImage = sampleLogo;

try {
// 로컬 이미지 에셋을 ArrayBuffer의 형태로 불러옴
const imageResponse = await fetch(targetLocalImage);
const imageBuffer = await imageResponse.arrayBuffer();
const contentType = 'image/png';

// 실제 이미지 응답처럼 ArrayBuffer와 헤더를 반환
return HttpResponse.arrayBuffer(imageBuffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'max-age=31536000, immutable', // S3-like cache header
},
});
} catch (error) {
console.error('Failed to load mock image asset:', error);
return new HttpResponse(null, { status: 500 });
}
Comment thread
i-meant-to-be marked this conversation as resolved.
}),
];
39 changes: 22 additions & 17 deletions src/page/LandingPage/components/TemplateCard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { useTranslation } from 'react-i18next';
import { DebateTemplate } from '../../../type/type';
import { Organization } from '../../../type/type';
import clsx from 'clsx';
import { createTableShareUrlFromEncodedData } from '../../../util/arrayEncoding';

interface TemplateCardProps {
organization: Organization;
className?: string; // 카드의 추가 className이 필요하면 사용
}

export default function TemplateCard({
title,
subtitle,
logoSrc,
actions,
className,
}: DebateTemplate) {
organization,
className = '',
}: TemplateCardProps) {
const { t } = useTranslation();
const logoUrl = import.meta.env.VITE_API_BASE_URL + organization.iconPath;
Comment thread
i-meant-to-be marked this conversation as resolved.

return (
<article
className={clsx(
Expand All @@ -23,36 +28,36 @@ export default function TemplateCard({
<div className="flex justify-between">
<div className="flex flex-col gap-2">
<h3 className="text-[min(max(0.9rem,1.4vw),1.1rem)] font-semibold leading-tight">
{title}
{organization.organization}
</h3>
<p className="mt-1 min-h-[1.25rem] text-[min(max(0.75rem,1.1vw),0.9rem)] text-neutral-700">
{subtitle ?? ''}
{organization.affiliation}
</p>
</div>

{/* 로고 */}
{logoSrc && (
{organization.iconPath && (
<img
src={logoSrc}
alt={t('{{title}} 로고', { title })}
src={logoUrl}
alt={t('{{title}} 로고', { title: organization.organization })}
className="h-12 w-12 shrink-0 object-contain"
/>
)}
</div>

{/* 액션 리스트 */}
<ul className="mt-5 flex flex-col gap-1">
{actions.map((action, index) => (
<li key={`${action.label}-${index}`}>
{organization.templates.map((template, index) => (
<li key={`${template.name}-${index}`}>
<div className="flex items-center justify-between gap-3 rounded-md bg-white px-3 py-2">
<span className="truncate text-[min(max(0.75rem,1.1vw),0.9rem)] font-medium text-neutral-800">
{action.label}
{template.name}
</span>

<a
href={action.href}
href={createTableShareUrlFromEncodedData(template.data)}
className="shrink-0 rounded-full border border-neutral-300 bg-brand px-4 py-1.5 text-[min(max(0.75rem,1.1vw),0.9rem)] font-medium text-default-black transition-colors duration-100 hover:bg-semantic-table hover:text-white"
aria-label={t('{{label}} 토론하기', { label: action.label })}
aria-label={t('{{label}} 토론하기', { label: template.name })}
>
{t('토론하기')}
</a>
Expand Down
14 changes: 9 additions & 5 deletions src/page/LandingPage/components/TemplateList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { DebateTemplate } from '../../../type/type';
import { Organization } from '../../../type/type';
import TemplateCard from './TemplateCard';

interface TemplateListProps {
data: DebateTemplate[];
organizations: Organization[];
}
export default function TemplateList({ data }: TemplateListProps) {

export default function TemplateList({ organizations }: TemplateListProps) {
return (
<div
className={'grid grid-cols-2 gap-5 lg:grid-cols-3'} // 2열, lg에서 3열
>
{data.map((template) => (
<TemplateCard key={template.title} {...template} />
{organizations.map((organization) => (
<TemplateCard
key={`${organization.organization}-${organization.affiliation}`}
organization={organization}
/>
Comment thread
i-meant-to-be marked this conversation as resolved.
))}
</div>
);
Expand Down
52 changes: 46 additions & 6 deletions src/page/LandingPage/components/TemplateSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
import { useTranslation } from 'react-i18next';
import { DEBATE_TEMPLATE } from '../../../constants/debate_template';
import TemplateApplicationSection from './TemplateApplicationSection';
import TemplateList from './TemplateList';
import { useGetOrganizationTemplates } from '../../../hooks/query/useGetOrganizationTemplates';
import LoadingSpinner from '../../../components/LoadingSpinner';
import ErrorIndicator from '../../../components/ErrorIndicator/ErrorIndicator';

export default function TemplateSelection() {
const { t } = useTranslation();
const { data, isError } = useGetOrganizationTemplates();

return (
<section id="template-selection" className="flex flex-col gap-12">
<div>
<h2 className="mt-4 text-left text-[min(max(1.25rem,2.75vw),2.5rem)] font-bold">
{t('다양한 토론 템플릿을 원클릭으로 만나보세요!')}
</h2>
</div>
<TemplateList data={DEBATE_TEMPLATE.ONE} />
<div className="mx-auto h-px w-11/12 bg-neutral-200" /> {/* 구분선 */}
<TemplateList data={DEBATE_TEMPLATE.TWO} />
<div className="mx-auto h-px w-11/12 bg-neutral-200" />
<TemplateList data={DEBATE_TEMPLATE.THREE} />
{isError ? (
<div className="flex w-full flex-col items-center justify-center space-y-[8px]">
<ErrorIndicator>
{t('템플릿을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.')}
</ErrorIndicator>
</div>
) : data ? (
<>
<TemplateList
key="template-list-1"
organizations={data.organizations.filter(
(orgs) => orgs.templates.length == 1,
)}
/>
<div className="mx-auto h-px w-11/12 bg-neutral-200" />
<TemplateList
key="template-list-2"
organizations={data.organizations.filter(
(orgs) => orgs.templates.length == 2,
)}
/>
<div className="mx-auto h-px w-11/12 bg-neutral-200" />
<TemplateList
key="template-list-3"
organizations={data.organizations.filter(
(orgs) => orgs.templates.length >= 3,
)}
/>
Comment thread
i-meant-to-be marked this conversation as resolved.
</>
) : (
<div className="flex w-full flex-col items-center justify-center space-y-[8px]">
<LoadingSpinner
strokeWidth={3}
size={'size-24'}
color={'text-default-disabled/hover'}
/>
<p className="text-center text-gray-500">
{t('템플릿을 불러오는 중입니다...')}
</p>
</div>
)}
<TemplateApplicationSection />
</section>
);
Expand Down
4 changes: 2 additions & 2 deletions src/page/LandingPage/hooks/useLandingPageHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
import { isLoggedIn } from '../../../util/accessToken';
import { oAuthLogin } from '../../../util/googleAuth';
import useLogout from '../../../hooks/mutations/useLogout';
import { createTableShareUrl } from '../../../util/arrayEncoding';
import { createTableShareUrlFromTable } from '../../../util/arrayEncoding';
import { SAMPLE_TABLE_DATA } from '../../../constants/sample_table';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -25,7 +25,7 @@ const useLandingPageHandlers = () => {
// Declare functions that represent business logics
const handleStartWithoutLogin = useCallback(() => {
// window.location.href = LANDING_URLS.START_WITHOUT_LOGIN_URL;
window.location.href = createTableShareUrl(
window.location.href = createTableShareUrlFromTable(
import.meta.env.VITE_SHARE_BASE_URL,
SAMPLE_TABLE_DATA,
);
Expand Down
Loading
Loading