Skip to content

Commit 9aefdcc

Browse files
authored
Merge pull request #22 from pythonkr/feature/add-map-component
feat: 지도 컴포넌트 추가
2 parents 14c1a31 + 7d8455f commit 9aefdcc

File tree

13 files changed

+323
-10
lines changed

13 files changed

+323
-10
lines changed

apps/pyconkr-admin/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@
3232
user-scalable=no,
3333
shrink-to-fit=no" />
3434
<meta name="author" content="PyCon Korea Organizing Team" />
35-
<meta name="description" content="Teaser site for PyCon Korea 2025" />
35+
<meta name="description" content="Admin for PyCon Korea" />
3636
<meta name="keywords" content="PyCon, Python, Conference, Korea, 2025" />
3737
<meta name="google" content="notranslate" />
3838
<meta name="googlebot" content="index, follow" />
3939
<meta name="robots" content="index, follow" />
4040

41+
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
4142
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
4243

4344
<style>

apps/pyconkr-admin/src/consts/mdx_components.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// 후대의 개발자님께 : 컴포넌트 맨 첫글자가 대문자로 시작하지 않으면 JSX 컴포넌트가 아니라 일반 HTML 태그로 인식합니다. 제발 대문자로 시작해주세요.
2+
import * as Common from "@frontend/common";
23
import * as Shop from "@frontend/shop";
34
import * as mui from "@mui/material";
45
import type { MDXComponents } from "mdx/types.js";
@@ -129,6 +130,10 @@ const MUIMDXComponents: MDXComponents = {
129130
Mui__material__Zoom: mui.Zoom,
130131
};
131132

133+
const PyConKRCommonMDXComponents: MDXComponents = {
134+
Common__Components__MDX__Map: Common.Components.MDX.Map,
135+
};
136+
132137
const PythonKRShopMDXComponents: MDXComponents = {
133138
Shop__Common__PriceDisplay: Shop.Components.Common.PriceDisplay,
134139
Shop__Common__SignInGuard: Shop.Components.Common.SignInGuard,
@@ -141,5 +146,6 @@ const PythonKRShopMDXComponents: MDXComponents = {
141146

142147
export const PyConKRMDXComponents = {
143148
...MUIMDXComponents,
149+
...PyConKRCommonMDXComponents,
144150
...PythonKRShopMDXComponents,
145151
};

apps/pyconkr/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<meta name="googlebot" content="index, follow" />
3939
<meta name="robots" content="index, follow" />
4040

41+
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
4142
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
4243

4344
<title>PyCon Korea 2025</title>

apps/pyconkr/src/components/pages/test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Button, Stack } from "@mui/material";
22
import * as React from "react";
33

44
import { BackendTestPage } from "../../debug/page/backend_test";
5+
import { MapTestPage } from "../../debug/page/map_test";
56
import { MdiTestPage } from "../../debug/page/mdi_test";
67
import { ShopTestPage } from "../../debug/page/shop_test";
78

89
const LOCAL_STORAGE_KEY = "selectedTab";
9-
type SelectedTabType = "shop" | "mdi" | "backend";
10+
type SelectedTabType = "shop" | "mdi" | "backend" | "map";
1011

1112
const getTabFromLocalStorage = (): SelectedTabType =>
1213
(localStorage.getItem(LOCAL_STORAGE_KEY) as SelectedTabType) || "shop";
@@ -20,6 +21,7 @@ const TabList: { [key in SelectedTabType]: React.ReactNode } = {
2021
shop: <ShopTestPage />,
2122
mdi: <MdiTestPage />,
2223
backend: <BackendTestPage />,
24+
map: <MapTestPage />,
2325
};
2426

2527
export const Test: React.FC = () => {
@@ -32,7 +34,7 @@ export const Test: React.FC = () => {
3234
);
3335

3436
return (
35-
<Stack>
37+
<Stack sx={{ width: "100%", height: "100%", minHeight: "100%", flexGrow: 1, py: 2 }} spacing={2}>
3638
<Stack direction="row" spacing={2} sx={{ width: "100%", justifyContent: "center" }}>
3739
{Object.keys(TabList).map((tab) => (
3840
<TabButton key={tab} tab={tab as SelectedTabType} />

apps/pyconkr/src/consts/mdx_components.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// 후대의 개발자님께 : 컴포넌트 맨 첫글자가 대문자로 시작하지 않으면 JSX 컴포넌트가 아니라 일반 HTML 태그로 인식합니다. 제발 대문자로 시작해주세요.
2+
import * as Common from "@frontend/common";
23
import * as Shop from "@frontend/shop";
34
import * as mui from "@mui/material";
45
import type { MDXComponents } from "mdx/types.js";
@@ -129,6 +130,10 @@ const MUIMDXComponents: MDXComponents = {
129130
Mui__material__Zoom: mui.Zoom,
130131
};
131132

133+
const PyConKRCommonMDXComponents: MDXComponents = {
134+
Common__Components__MDX__Map: Common.Components.MDX.Map,
135+
};
136+
132137
const PythonKRShopMDXComponents: MDXComponents = {
133138
Shop__Common__PriceDisplay: Shop.Components.Common.PriceDisplay,
134139
Shop__Common__SignInGuard: Shop.Components.Common.SignInGuard,
@@ -141,5 +146,6 @@ const PythonKRShopMDXComponents: MDXComponents = {
141146

142147
export const PyConKRMDXComponents = {
143148
...MUIMDXComponents,
149+
...PyConKRCommonMDXComponents,
144150
...PythonKRShopMDXComponents,
145151
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as Common from "@frontend/common";
2+
import { Box, Button, FormControlLabel, Stack, Switch, TextField } from "@mui/material";
3+
import * as React from "react";
4+
5+
type MapTestPageStateType = {
6+
checked: boolean;
7+
mapProps: Common.Components.MDX.MapPropType;
8+
};
9+
10+
const INITIAL_DATA: Common.Components.MDX.MapPropType = {
11+
language: "ko",
12+
geo: { lat: 37.5580918, lng: 126.9982178 },
13+
placeName: {
14+
ko: "동국대학교 신공학관",
15+
en: "Dongguk University\nNew Engineering Building",
16+
},
17+
placeCode: {
18+
kakao: "17579989",
19+
google: "gryFHrZub6tsXdwb9",
20+
naver: "5IieESq8",
21+
},
22+
googleMapIframeSrc:
23+
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3162.871473157695!2d126.99821779999999!3d37.5580918!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x357ca302befa0c31%3A0xbc66c66731962172!2z64-Z6rWt64yA7ZWZ6rWQIOyLoOqzte2Vmeq0gA!5e0!3m2!1sko!2sen!4v1748768615566!5m2!1sko!2sen",
24+
};
25+
26+
export const MapTestPage: React.FC = () => {
27+
const geoFormRef = React.useRef<HTMLFormElement>(null);
28+
const placeNameFormRef = React.useRef<HTMLFormElement>(null);
29+
const placeCodeFormRef = React.useRef<HTMLFormElement>(null);
30+
const gMapIframeUrlInputRef = React.useRef<HTMLInputElement>(null);
31+
32+
const [state, setState] = React.useState<MapTestPageStateType>({ checked: false, mapProps: INITIAL_DATA });
33+
const setChecked = (checked: boolean) => setState((ps) => ({ ...ps, checked }));
34+
const language = state.checked ? "en" : "ko";
35+
36+
const onApply = () => {
37+
const geoForm = geoFormRef.current;
38+
const pNameForm = placeNameFormRef.current;
39+
const pCodeForm = placeCodeFormRef.current;
40+
const gMapIframeUrl = gMapIframeUrlInputRef.current;
41+
42+
[geoForm, pNameForm, pCodeForm, gMapIframeUrl].forEach((formOrInput, index) => {
43+
if (!formOrInput) throw new Error(`${formOrInput}[${index}] is not defined.`);
44+
45+
if (formOrInput instanceof HTMLFormElement && !Common.Utils.isFormValid(formOrInput))
46+
throw new Error(`${formOrInput}[${index}] is not valid.`);
47+
48+
if (formOrInput instanceof HTMLInputElement && !formOrInput.checkValidity())
49+
throw new Error(`${formOrInput}[${index}] is not valid.`);
50+
});
51+
if (!(geoForm && pNameForm && pCodeForm && gMapIframeUrl)) return;
52+
53+
const strGeo = Common.Utils.getFormValue<{ lat: string; lng: string }>({ form: geoForm });
54+
if (!strGeo.lat || !strGeo.lng || isNaN(parseFloat(strGeo.lat)) || isNaN(parseFloat(strGeo.lng))) {
55+
alert("위도와 경도를 올바르게 입력해주세요.");
56+
return;
57+
}
58+
const geo = { lat: parseFloat(strGeo.lat), lng: parseFloat(strGeo.lng) };
59+
const googleMapIframeSrc = gMapIframeUrl.value.trim();
60+
const placeCode = Common.Utils.getFormValue<{ kakao: string; naver: string; google: string }>({ form: pCodeForm });
61+
const placeName = Common.Utils.getFormValue<{ ko: string; en: string }>({ form: pNameForm });
62+
placeName.ko = placeName.ko.trim().replace("\\n", "\n");
63+
placeName.en = placeName.en.trim().replace("\\n", "\n");
64+
65+
setState((ps) => ({ ...ps, mapProps: { language, geo, placeCode, placeName, googleMapIframeSrc } }));
66+
};
67+
68+
return (
69+
<Stack direction="row" spacing={2} sx={{ p: 2 }}>
70+
<Stack spacing={2} sx={{ width: "50%", maxWidth: "50%" }}>
71+
<FormControlLabel
72+
control={<Switch checked={state.checked} onChange={(e) => setChecked(e.target.checked)} />}
73+
label={language}
74+
/>
75+
<form ref={geoFormRef}>
76+
<Stack spacing={1}>
77+
<TextField label="Latitude" name="lat" defaultValue={state.mapProps.geo.lat} />
78+
<TextField label="Longitude" name="lng" defaultValue={state.mapProps.geo.lng} />
79+
</Stack>
80+
</form>
81+
<form ref={placeNameFormRef}>
82+
<Stack spacing={1}>
83+
<TextField label="명칭 (KR)" name="ko" defaultValue={state.mapProps.placeName.ko} />
84+
<TextField label="명칭 (EN)" name="en" defaultValue={state.mapProps.placeName.en} />
85+
</Stack>
86+
</form>
87+
<form ref={placeCodeFormRef}>
88+
<Stack spacing={1}>
89+
<TextField label="Kakaomap Code" name="kakao" defaultValue={state.mapProps.placeCode.kakao} />
90+
<TextField label="Google Maps Code" name="google" defaultValue={state.mapProps.placeCode.google} />
91+
<TextField label="Naver Shortened URL Code" name="naver" defaultValue={state.mapProps.placeCode.naver} />
92+
</Stack>
93+
</form>
94+
<TextField
95+
label="Google Map Iframe URL"
96+
type="url"
97+
inputRef={gMapIframeUrlInputRef}
98+
defaultValue={state.mapProps.googleMapIframeSrc}
99+
/>
100+
<Button onClick={onApply}>적용</Button>
101+
</Stack>
102+
<Box sx={{ width: "50%", maxWidth: "50%" }}>
103+
<Common.Components.MDX.Map {...state.mapProps} />
104+
</Box>
105+
</Stack>
106+
);
107+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@tanstack/react-query-devtools": "^5.76.1",
6161
"@types/crypto-js": "^4.2.2",
6262
"@types/json-schema": "^7.0.15",
63+
"@types/kakaomaps": "^1.1.5",
6364
"@types/mdx": "^2.0.13",
6465
"@types/node": "^22.15.18",
6566
"@types/react": "^19.1.4",

packages/common/src/components/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
} from "./dynamic_route";
88
import { ErrorFallback as ErrorFallbackComponent } from "./error_handler";
99
import { MDXRenderer as MDXRendererComponent } from "./mdx";
10+
import type { MapPropType as MapComponentPropType } from "./mdx_components/map";
11+
import { Map as MapComponent } from "./mdx_components/map";
1012
import { MDXEditor as MDXEditorComponent } from "./mdx_editor";
1113
import { PythonKorea as PythonKoreaComponent } from "./pythonkorea";
1214

@@ -20,6 +22,11 @@ namespace Components {
2022
export const MDXRenderer = MDXRendererComponent;
2123
export const PythonKorea = PythonKoreaComponent;
2224
export const ErrorFallback = ErrorFallbackComponent;
25+
26+
export namespace MDX {
27+
export const Map = MapComponent;
28+
export type MapPropType = MapComponentPropType;
29+
}
2330
}
2431

2532
export default Components;

packages/common/src/components/mdx.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ import * as R from "remeda";
2020

2121
import Hooks from "../hooks";
2222
import { ErrorFallback } from "./error_handler";
23+
import { rtrim } from "../utils/string";
24+
25+
const REGISTERED_KEYWORDS = [
26+
"import",
27+
"export",
28+
"const",
29+
"let",
30+
"function",
31+
"class",
32+
"if",
33+
"else",
34+
"for",
35+
"while",
36+
"return",
37+
"switch",
38+
"case",
39+
"break",
40+
"continue",
41+
",",
42+
";",
43+
"{",
44+
"}",
45+
];
2346

2447
const CustomMDXComponents: MDXComponents = {
2548
h1: (props) => <h1 {...props} />,
@@ -47,13 +70,15 @@ const CustomMDXComponents: MDXComponents = {
4770
};
4871

4972
const lineFormatterForMDX = (line: string) => {
50-
const trimmedLine = line.trim();
73+
if (R.isEmpty(line.trim())) return "\n";
5174

52-
if (R.isEmpty(trimmedLine)) return "\n";
75+
const trimmedLine = rtrim(line);
5376

54-
// import문을 위한 꼼수 - import문 다음 줄은 반드시 빈 줄이어야 합니다.
77+
// import / export / const문을 위한 꼼수 - import문 다음 줄은 반드시 빈 줄이어야 합니다.
5578
// 그러나 \n\n으로 변환할 경우, 다음 단계에서 <br />로 변환되므로, import문 다음에 공백이 있는 줄을 넣어서 <br />로 변환되지 않도록 합니다.
56-
if (trimmedLine.startsWith("import")) return `${trimmedLine}\n \n`;
79+
if (REGISTERED_KEYWORDS.some((keyword) => trimmedLine.startsWith(keyword) || trimmedLine.endsWith(keyword))) {
80+
return `${trimmedLine}\n \n`;
81+
}
5782

5883
// Table인 경우, 뒤에 공백을 추가하지 않습니다.
5984
if (trimmedLine.startsWith("|") && trimmedLine.endsWith("|")) return `${trimmedLine}\n`;

0 commit comments

Comments
 (0)