diff --git a/.env.template b/.env.template index 480d766..6282d32 100644 --- a/.env.template +++ b/.env.template @@ -21,3 +21,9 @@ KAKAO_REDIRECT_URI= NAVER_CLIENT_ID= NAVER_CLIENT_SECRET= NAVER_REDIRECT_URI= + +## KAKAO MAP +### @see https://developers.kakao.com/console/app/1102339/config/appKey +KAKAO_MAP_BASE_URL= +KAKAO_MAP_API_KEY= +DAUMCDN_POSTOCDE_URL= \ No newline at end of file diff --git a/package.json b/package.json index f552aec..c9828f2 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "invi", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "dev": "next dev", "build": "next build", @@ -39,6 +40,7 @@ "@tanstack/react-form": "^0.26.1", "@tanstack/react-query": "^5.50.1", "@tanstack/zod-form-adapter": "^0.25.3", + "@types/uuid": "^10.0.0", "arctic": "^2.0.0-next.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -53,6 +55,7 @@ "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", + "uuid": "^10.0.0", "zod": "^3.23.8", "zustand": "^4.5.4" }, diff --git a/src/app/(playground)/playground/map/_components/AddressSearchButton/index.tsx b/src/app/(playground)/playground/map/_components/AddressSearchButton/index.tsx new file mode 100644 index 0000000..3d6c6ab --- /dev/null +++ b/src/app/(playground)/playground/map/_components/AddressSearchButton/index.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useKakaoAddress } from "~/app/(playground)/playground/map/_components/KakaoAddressContext"; + +export default function AddressSearchButton() { + const { setCoordinate } = useKakaoAddress(); + + const handleClickButton = () => { + new window.daum.Postcode({ + oncomplete: (addressData: any) => { + const geocoder = new window.kakao.maps.services.Geocoder(); + geocoder.addressSearch( + addressData.address, + (result: any, status: any) => { + const currentPositions = new window.kakao.maps.LatLng( + result[0].y, + result[0].x, + ); + setCoordinate(currentPositions.Ma, currentPositions.La); + }, + ); + }, + }).open(); + }; + + return ( + + 내 주소 찾기 + + ); +} diff --git a/src/app/(playground)/playground/map/_components/KakaoAddressContext/index.tsx b/src/app/(playground)/playground/map/_components/KakaoAddressContext/index.tsx new file mode 100644 index 0000000..28207b1 --- /dev/null +++ b/src/app/(playground)/playground/map/_components/KakaoAddressContext/index.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { ReactNode } from "react"; +import { createContext, useContext, useState } from "react"; + +export interface Coordinate { + latitude: number; + longitude: number; +} + +interface KakaoAddressContextType { + coordinate: Coordinate; + setCoordinate: (latitude: number, longitude: number) => void; +} + +const KakaoAddressContext = createContext( + undefined, +); + +export function KakaoAddressProvider({ children }: { children: ReactNode }) { + const [coordinate, setCoordinateState] = useState({ + latitude: 37.566828, + longitude: 126.9786567, + }); + + const setCoordinate = (latitude: number, longitude: number) => { + setCoordinateState({ latitude, longitude }); + }; + + return ( + + {children} + + ); +} + +export function useKakaoAddress() { + const context = useContext(KakaoAddressContext); + if (context === undefined) { + throw new Error( + "useKakaoAddress must be used within a KakaoAddressProvider", + ); + } + return context; +} diff --git a/src/app/(playground)/playground/map/_components/KakaoMap/index.tsx b/src/app/(playground)/playground/map/_components/KakaoMap/index.tsx new file mode 100644 index 0000000..1da4e3b --- /dev/null +++ b/src/app/(playground)/playground/map/_components/KakaoMap/index.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { useKakaoAddress } from "~/app/(playground)/playground/map/_components/KakaoAddressContext"; + +interface KakaoMapProps { + width: string; + height: string; + level?: number; + addCenterPin?: boolean; + latitude?: number; + longitude?: number; +} + +export default function KakaoMap({ + width, + height, + level = 3, + addCenterPin = false, + latitude, + longitude, +}: KakaoMapProps) { + const { coordinate } = useKakaoAddress(); + const mapRef = useRef(null); + const mapInstanceRef = useRef(null); + const markerRef = useRef(null); + + useEffect(() => { + if (!mapRef.current) return; + + const initializeMap = () => { + const center = new window.kakao.maps.LatLng( + latitude ?? coordinate.latitude, + longitude ?? coordinate.longitude, + ); + + const mapInstance = new window.kakao.maps.Map( + mapRef.current as HTMLElement, + { + center, + level, + }, + ); + + mapInstanceRef.current = mapInstance; + + if (addCenterPin) { + const marker = new window.kakao.maps.Marker({ position: center }); + marker.setMap(mapInstance); + markerRef.current = marker; + } + }; + + if (window.kakao && window.kakao.maps) { + window.kakao.maps.load(initializeMap); + } + }, []); + + useEffect(() => { + if (mapInstanceRef.current) { + const newCenter = new window.kakao.maps.LatLng( + latitude ?? coordinate.latitude, + longitude ?? coordinate.longitude, + ); + mapInstanceRef.current.setCenter(newCenter); + + if (addCenterPin && markerRef.current) { + markerRef.current.setPosition(newCenter); + } + } + }, [coordinate]); + + return ( + + + + ); +} diff --git a/src/app/(playground)/playground/map/_components/MapUtilsButton/UtilsContainer/index.tsx b/src/app/(playground)/playground/map/_components/MapUtilsButton/UtilsContainer/index.tsx new file mode 100644 index 0000000..398f5b4 --- /dev/null +++ b/src/app/(playground)/playground/map/_components/MapUtilsButton/UtilsContainer/index.tsx @@ -0,0 +1 @@ +export default function UtilsContainer() {} diff --git a/src/app/(playground)/playground/map/_components/MapUtilsButton/index.tsx b/src/app/(playground)/playground/map/_components/MapUtilsButton/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(playground)/playground/map/layout.tsx b/src/app/(playground)/playground/map/layout.tsx new file mode 100644 index 0000000..5c94016 --- /dev/null +++ b/src/app/(playground)/playground/map/layout.tsx @@ -0,0 +1,24 @@ +import Script from "next/script"; +import { env } from "~/lib/env"; + +export default function KaKaoMapPageLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + + + {children} + > + ); +} diff --git a/src/app/(playground)/playground/map/page.tsx b/src/app/(playground)/playground/map/page.tsx new file mode 100644 index 0000000..114d56b --- /dev/null +++ b/src/app/(playground)/playground/map/page.tsx @@ -0,0 +1,37 @@ +import { AMain } from "~/app/(playground)/playground/inner-tools"; +import AddressSearchButton from "~/app/(playground)/playground/map/_components/AddressSearchButton"; +import { KakaoAddressProvider } from "~/app/(playground)/playground/map/_components/KakaoAddressContext"; +import KakaoMap from "~/app/(playground)/playground/map/_components/KakaoMap"; + +export default function KaKaoMapPage() { + return ( + + + 결과 페이지에서 보여줄 위도 경도 입력한 지도 + + + + + + Provider Test 1 + + + + + + + Provider Test 2 + + + + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 5a16ff5..9b54e39 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,3 @@ export default function Home() { - return 인비 🐝; + return 인비 🐝🐝; } diff --git a/src/lib/db/migrations/0004_lying_swordsman.sql b/src/lib/db/migrations/0004_lying_swordsman.sql new file mode 100644 index 0000000..b000694 --- /dev/null +++ b/src/lib/db/migrations/0004_lying_swordsman.sql @@ -0,0 +1 @@ +ALTER TABLE "invitation_response" ALTER COLUMN "id" SET DATA TYPE text; \ No newline at end of file diff --git a/src/lib/db/migrations/meta/0004_snapshot.json b/src/lib/db/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..d306bba --- /dev/null +++ b/src/lib/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,244 @@ +{ + "id": "f5ad7474-391e-414a-912d-2fb1cb551851", + "prevId": "1e7681b2-7222-4b1a-a6a5-eb338ef65ce8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.invitation_response": { + "name": "invitation_response", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "participant_name": { + "name": "participant_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attendance": { + "name": "attendance", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.test_job": { + "name": "test_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "test_id": { + "name": "test_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "test_job_test_id_test_id_fk": { + "name": "test_job_test_id_test_id_fk", + "tableFrom": "test_job", + "tableTo": "test", + "columnsFrom": ["test_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.test": { + "name": "test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "test_email_unique": { + "name": "test_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + } + } + }, + "enums": { + "public.provider": { + "name": "provider", + "schema": "public", + "values": ["google", "kakao", "naver"] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/lib/db/schema/invitation_response.query.ts b/src/lib/db/schema/invitation_response.query.ts index b4ea739..c750c8b 100644 --- a/src/lib/db/schema/invitation_response.query.ts +++ b/src/lib/db/schema/invitation_response.query.ts @@ -1,10 +1,12 @@ "use server"; import { count, eq, sql, sum } from "drizzle-orm"; +import { nanoid } from "nanoid"; import { db } from "~/lib/db"; import { invitationResponses, type InvitationResponse, + type InvitationResponseInsert, } from "~/lib/db/schema/invitation_response"; export async function getAllInvitationResponses() { @@ -34,3 +36,20 @@ export async function getInvitationResponseStats() { attendingCount: Number(result[0].attendingCount), }; } + +export async function createInvitationResponses( + participant_name: string, + attendance: boolean, + reason: string, +) { + const data: InvitationResponseInsert = { + id: nanoid(), + participant_name: participant_name, + attendance: attendance, + reason: reason, + created_at: new Date(), + }; + + const res = db.insert(invitationResponses).values(data); + return res; +} diff --git a/src/lib/db/schema/invitation_response.ts b/src/lib/db/schema/invitation_response.ts index b1ff852..ba95b88 100644 --- a/src/lib/db/schema/invitation_response.ts +++ b/src/lib/db/schema/invitation_response.ts @@ -1,7 +1,7 @@ -import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; export const invitationResponses = pgTable("invitation_response", { - id: uuid("id").primaryKey().notNull(), + id: text("id").primaryKey().notNull(), participant_name: text("participant_name").notNull(), attendance: boolean("attendance").notNull(), reason: text("reason"), diff --git a/src/lib/env.ts b/src/lib/env.ts index d73395e..43afe43 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -17,6 +17,10 @@ export const env = createEnv({ NAVER_CLIENT_ID: z.string(), NAVER_CLIENT_SECRET: z.string(), NAVER_REDIRECT_URI: z.string(), + /* ---- KaKao Map ---- */ + KAKAO_MAP_BASE_URL: z.string(), + KAKAO_MAP_API_KEY: z.string(), + DAUMCDN_POSTOCDE_URL: z.string(), }, client: {}, experimental__runtimeEnv: {}, diff --git a/src/types/global/kakao.d.ts b/src/types/global/kakao.d.ts new file mode 100644 index 0000000..6c1ac9b --- /dev/null +++ b/src/types/global/kakao.d.ts @@ -0,0 +1,24 @@ +declare global { + interface Window { + daum: { + Postcode: new (options: any) => any; + }, + kakao: { + maps: { + load: (callback: () => void) => void; + LatLng: new (lat: number, lng: number) => any; + Map: new (container: HTMLElement, options: any) => any; + Marker: new (options: any) => { + setMap: (map: any) => void; + }; + services: { + Geocoder: new () => { + addressSearch: (address: string, callback: (result: any, status: any) => void) => void; + }; + }; + }; + }; + } +} + +export {}; \ No newline at end of file
결과 페이지에서 보여줄 위도 경도 입력한 지도
Provider Test 1
Provider Test 2