Creative Code

Lunar-(8)로그인화면,지도기능 업데이트(지도 표시, 지도 키워드 검색, 자동차 길찾기 기능) 본문

Projects

Lunar-(8)로그인화면,지도기능 업데이트(지도 표시, 지도 키워드 검색, 자동차 길찾기 기능)

빛하루 2023. 12. 20. 01:26

pages/login.py

--> 로그인화면의 배경화면을 gif 파일로 수정

 

※pages/login.py 코드

"""Login page. Uses auth_layout to render UI shared with the sign up page."""
import reflex as rx

from lunar.state.auth import AuthState

def login():
    return rx.container(
        rx.container(height='150px'),
        rx.hstack( 
            # rx.vstack(
            #     rx.container(height='30px'),
            #     rx.image(
            #         src = '/space2.jpg',
            #     ),
            #     width = '500px',
            #     height= '100%',
            # ),
            # rx.container(width='30px'),
            rx.vstack(
                rx.hstack(
                    rx.vstack(
                        rx.container(height='20px'),
                        rx.image(
                            src = "/moonico.ico",
                            width="70px",
                            height="70px",
                        ),
                    ),              
                    rx.vstack(           
                        rx.container(height='8px'),
                        rx.container(
                            rx.text(
                                "Lunar",
                                style={
                                    "fontSize": "50px",
                                    "fontWeight": "bolder",
                                    "letterSpacing": "3px",
                                    "fontFamily": "Comic Sans MS, Cursive",
                                    "background": "-webkit-linear-gradient(-45deg, #e04a3f, #24d6d6)",
                                    "-webkit-background-clip": "text",
                                    "color": "black",
                                },
                                mb=-3,
                            ),
                            rx.text(
                                "Share your daily life with people!",
                                style={
                                    'background': "-webkit-linear-gradient(-45deg, #e04a3f, #4e8be6)",
                                    'background_clip': "text",  # 텍스트에만 그라데이션 적용
                                    'color': "transparent",  # 텍스트 색상을 투명으로 설정
                                    'font_weight': "bold",
                                    'fontSize':'20px',
                                },
                            ),
                        ),

                    ),
                ),
                rx.container(height='20px'),
                rx.container(
                    rx.vstack(
                        rx.container(
                            rx.input(placeholder="Nickname", on_blur=AuthState.set_username, mb=4),
                            rx.input(
                                type_="password",
                                placeholder="Password",
                                on_blur=AuthState.set_password,
                                mb=4,
                            ),
                            rx.button(
                                "Log in",
                                on_click=AuthState.login,
                                bg="#212963",
                                color="white",
                                _hover={"bg": "blue.600"},
                            ),
                            center_content=True,
                            align_items="left",
                            bg="white",
                            border="1px solid #eaeaea",
                            p=4,
                            max_width="400px",
                            border_radius="20px",
                            background= 'rgb(255,255,255,0.7)'
                        ),
                        rx.container(height='10px'),
                        rx.vstack(
                            rx.text(
                                'Forgot your id?     ',
                                rx.link('Find Id!',href="/findid",color='red.500'),
                                color='gray.600', 
                            ),
                            rx.text(
                                'Forgot your password?     ',
                                rx.link('Find Password!',href="/findpassword",color='green.500'),
                                color="gray.600",
                            ), 
                            rx.text(
                                "Don't have an account yet?     ",
                                rx.link("Sign up!", href="/signup", color="blue.500"),
                                color="gray.600",
                            ),
                            align_items='start',
                        ),
                        rx.container(height='30px') ,  
                    ),
                ),
                width='500px',
                height='auto',
                center_content=True,
                borderRadius='40px',
                boxShadow='10px 10px 100px #79d0ed',
                background= 'rgb(255,255,255,0.9)'
            ),
        ),
        center_content=True,
        # justifyContent='center',
        maxWidth='auto',
        maxHeight='auto',
        height='100vh',
        background_image = '/deepspace1.gif',
        background_size = 'cover',
        # background_repeat = 'no-repeat',
        # style={
        #     'background-image':"url('/space2.jpg')",
        #     'background-size':'cover',
        # }
    )

 

pages/map.py

 

1. 지난번 Aurora프로젝트 때 구현에 실패했던 검색할때 마다 지도의 마커가 사라지지않는 현상이 수정완료되어 이제 지도와 키워드 검색단어와 관련된 장소들의 목록이 나열된 데이터프레임이 같이 제공된다!

 

2.오른쪽에서 출발지의 경도,위도좌표를 입력하고 목적지의 경도,위도를 차례대로 입력하면 출발지부터 목적지까지의 경로가 마커로 표시된다!(자동차 내비게이션 기능)

 

내일은 지도기능의 전체 UI수정을 진행해야 할 것같다.. 

 

※pages/map.py 전체 코드

# lunar.state.home 모듈에서 필요한 State 및 HomeState를 가져옵니다.
import reflex as rx
from lunar.state.base import State
from lunar.state.home import HomeState

# 컴포넌트를 가져옵니다.
from ..components import container

color = "rgb(107,99,246)"
# 탭 버튼을 생성하는 함수
def tab_button(imagepath, href):
    """A tab switcher button."""
    return rx.link(
        rx.image(
            src=imagepath,
            height='40px',
            width='40px',
        ),
        display="inline-flex",
        align_items="center",
        py=3,
        px=2,
        href=href,  # 버튼 클릭 시 이동할 경로
    )

# 왼쪽에 표시되는 탭 스위처
def tabs():
    """The tab switcher displayed on the left."""
    return rx.box(
        rx.vstack(
            rx.container(
                rx.hstack(
                    rx.image(
                        src='/moon.png',
                        height='60px',
                        width='60px',         
                    ),
                    rx.text(
                        "Lunar", 
                        style={
                            "fontSize": "40px",
                            "fontWeight": "bolder",
                            "fontFamily": "Calibri, Calibri",
                            "background": "-webkit-linear-gradient(-45deg, #e04a3f, #4e8be6)",
                            "-webkit-background-clip": "text",
                            "color": "transparent",
                        },
                        center_content=True,
                    ),  # 앱 이름
                ),
            ),
            rx.button(
                "Sign out",
                on_click=State.logout,
                bg="#212963",
                color="white",
                _hover={"bg": "blue.600"},
            ),
            rx.container(height='200px'),
            align_items="left",
            gap=4,
        ),
        py=4,
    )

# 오른쪽에 표시되는 사이드바
def sidebar(HomeState):
    """The sidebar displayed on the right."""
    return rx.vstack(
        rx.vstack(
            rx.image(src='/startpoint.png',height='35px',width='35px'),
            rx.input(
                on_change=HomeState.set_start_location_x,
                placeholder="starting point longitude", 
                width="100%",
                border = "3px solid #000000",
            ),
            rx.input(
                on_change=HomeState.set_start_location_y,
                placeholder="starting point latitude", 
                width="100%",
                border = "3px solid #000000",
            ),
            rx.image(src='/endpoint.png',height='35px',width='35px'),
            rx.input(
                on_change=HomeState.set_end_location_x,
                placeholder="end point longitude", 
                width="100%",
                border = "3px solid #000000",
            ),
            rx.input(
                on_change=HomeState.set_end_location_y,
                placeholder="end point latitude", 
                width="100%",
                border = "3px solid #000000",
            ),
            rx.hstack(
                rx.button(
                    'Search',
                    on_click = HomeState.get_directions,
                ),
                align_items='flex-end',
            ),
        ),
        align_items="start",
        gap=4,
        h="100%",
        py=4,
    )

# 피드의 헤더
def feed_header(HomeState):
    """The header of the feed."""
    return rx.hstack(
        rx.image(src='/find1.png',height='35px',width='35px'),
        rx.input(on_change=HomeState.set_map_search_input, placeholder="Search place..!"),
        rx.button('search', on_click = HomeState.map_search),
        rx.button('clear',on_click = HomeState.map_clear),
        justify="space-between",
        p=4,
        border_bottom="3px solid #000000",
    )

# 피드 영역
def feed(HomeState):
    """The feed."""
    return rx.box(
        feed_header(HomeState),
        rx.html(HomeState.time_map_iframe),
        rx.data_table(
            data=HomeState.df,
            font_size = '8px',
            width='100%',
        ),
        overflow='auto',
    )

# 홈 페이지
def map():
    State.check_login
    return rx.vstack(
        rx.grid(
            tabs(),
            feed(HomeState),
            sidebar(HomeState),
            grid_template_columns="2fr 5fr 2fr",
            width='97%',
            h="90vh",
            gap=4,
        ),
        rx.hstack(
            tab_button('/Home.png','/'),
            tab_button('/profile.png','/profile'),
            tab_button('/map.png','/map'),
            tab_button('/chat.png','/chat'),
            tab_button('/Aichat.png','/aichat'),
            tab_button('/diary.png','/diary'),
            tab_button('/video.png','/video'),
            tab_button('/game.png','/game'),
            tab_button('/setting.png','/setting'),
            margin_right='5px',
            border="1px solid #000000",
            border_radius="full",
        ),
        width='100%',
    )

 

 

다음은 위의 기능들이 동작하는 함수들이 저장된 코드

※state/home.py

"""The state for the home page."""
from datetime import datetime
import tkinter as tk
import reflex as rx
from sqlmodel import select
import os
from .base import Follows, State, Crater, User
from tkinter import filedialog
import folium
from folium.plugins import MiniMap
import requests
import pandas as pd
import numpy as np
import json
import asyncio


class HomeState(State):
    """The state for the home page."""

    # 데이터 베이스 저장된 crater 불러오기
    crater: str
    craters: list[Crater] = []

    # 친구,crater 검색
    friend: str
    search: str

    # 파일 선택 변수
    img: list[str]                                                             
    files: list[str] = []
    imgshow:bool=False

    # map 키워드 검색
    map_count:int=1
    map_search_input:str=''
    map_html:str='/map.html'
    map_iframe:str
    df : pd.DataFrame
    start_location_x:str
    start_location_y:str
    end_location_x:str
    end_location_y:str
    KAKAO_REST_API_KEY:str

    @rx.var
    def time_map_iframe(self)->str:
        self.map_iframe=f'<iframe src="{self.map_html}" width="100%" height="500px"></iframe>'
        return self.map_iframe

    def handle_file_selection(self):                                          
        root = tk.Tk()
        root.withdraw()  # 화면에 창을 보이지 않도록 함
        root.attributes('-topmost', True)
        file_paths = filedialog.askopenfilenames(master=root)

        # 선택된 파일 경로에 대한 처리
        for file_path in file_paths:
            file_name = os.path.basename(file_path)                           # 파일 이름과 확장자를 추출
            file_extension = os.path.splitext(file_name)[1]
            
            upload_data = open(file_path, "rb").read()                        # 선택한 파일을 저장
            outfile = f".web/public/{file_name}"

            # Save the file.
            with open(outfile, "wb") as file_object:
                file_object.write(upload_data)

            # Update the img var.
            self.img.append(file_name)

            # Set the files attribute
            self.files.append(file_name)
        if len(self.img)>0:
            self.change()

    # 파일 업로드 함수
    async def handle_upload(                                                 
        self, files: list[rx.UploadFile]
    ):
        for file in files:
            upload_data = await file.read()
            outfile = f"/{file.filename}"

            # Save the file.
            with open(outfile, "wb") as file_object:
                file_object.write(upload_data)

            # Update the img var.
            self.img.append(file.filename)

    # 파일선택창 화면띄우는 함수
    def change(self):
        self.imgshow = not (self.imgshow)

    # Crater 파일 선택 취소 함수        
    async def file_select_cancel(self):
        self.img=[]
        self.files=[]
        if len(self.img)>0:
            self.change()
    

    # Crater 업로드 함수
    async def post_crater(self):
        if not self.logged_in:
            return rx.window_alert("Please log in to post a crater.")                 # 로그인이 되어있지 않을 시 경고 메시지
        if len(self.crater)==0:
            return rx.window_alert('Please write at least one character!')           # story 추가시 최소 한 글자 입력 경고 메시지
        if len(self.crater)>70:
            return rx.window_alert('Please enter within 70 characters!')            # 150글자 이내로 입력제한
        
        await self.handle_upload(rx.upload_files())                                  # 이미지 추가
        
        with rx.session() as session:                                                # session에 생성한 story 모델 저장
            crater = Crater(
                author=self.user.username,                                           # author : 유저 아이디
                content=self.crater,                                                  # content : 유저 story 입력 내용
                created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),             # created_at : stroy 작성 시간
                image_content=", ".join(self.files),                                 # image_content : 이미지 파일
                heart_list='',
                comment_list='',
                heart_num=0,
                comment_num=0,
                
            )
            
            session.add(crater)
            session.commit()
            self.crater = ""                                                          # session에 저장 후 story내용 초기화
            self.img=[]
            self.files=[]
            
        return self.get_craters()

    # Crater 내용 불러오는 함수
    def get_craters(self):
        """Get craters from the database."""
        with rx.session() as session:
            if self.search:                                                          # story 검색 입력어가 있을경우
                self.craters = (
                    session.query(Crater)
                    .filter(Crater.content.contains(self.search))                     # story 검색 입력단어가 포함된 story를 모두 가져옴
                    .all()[::-1]
                )
            else:
                self.craters = session.query(Crater).all()[::-1]                       # session에 저장된 모든 story를 가져옴



    def set_search(self, search):
        """Set the search query."""
        self.search = search
        return self.get_craters()

    def follow_user(self, username):
        """Follow a user."""
        with rx.session() as session:
            friend = Follows(
                follower_username=self.user.username, followed_username=username
            )
            session.add(friend)
            session.commit()

    @rx.var
    def following(self) -> list[Follows]:
        """Get a list of users the current user is following."""
        if self.logged_in:
            with rx.session() as session:
                return session.exec(
                    select(Follows).where(
                        Follows.follower_username == self.user.username
                    )
                ).all()
        return []

    @rx.var
    def followers(self) -> list[Follows]:
        """Get a list of users following the current user."""
        if self.logged_in:
            with rx.session() as session:
                return session.exec(
                    select(Follows).where(
                        Follows.followed_username == self.user.username
                    )
                ).all()
        return []

    @rx.var
    def search_users(self) -> list[User]:
        """Get a list of users matching the search query."""
        if self.friend != "":
            with rx.session() as session:
                current_username = self.user.username if self.user is not None else ""
                users = session.exec(
                    select(User).where(
                        User.username.contains(self.friend),
                        User.username != current_username,
                    )
                ).all()
                return users
        return []
    
    # KaKao Rest API Key를 받아오는 함수     
    def kakao_api(self): 
        key=''
        with open('kakaoapikey.json','r')as f:                                               
            key = json.load(f)
        self.KAKAO_REST_API_KEY = key['key']
    
    # kakao api 검색으로 장소 목록을 받는 함수   
    def elec_location(self,region,page_num):
        self.kakao_api()                                                                    
        url = 'https://dapi.kakao.com/v2/local/search/keyword.json'
        params = {'query': region,'page': page_num}                                         
        headers = {"Authorization": f'KakaoAK {self.KAKAO_REST_API_KEY}'}                   
        places = requests.get(url, params=params, headers=headers).json()['documents']                                                                         
        return places  
    
    # 장소목록의 정보를 가져오는 함수
    def elec_info(self,places):
        X = []    # 경도                                                        
        Y = []    # 위도                                                                    
        stores = []                                                                        
        road_address = []                                                                   
        place_url = []                                                                      
        ID = []                                                                             

        for place in places:                                                                
            X.append(float(place['x']))
            Y.append(float(place['y']))
            stores.append(place['place_name'])
            road_address.append(place['road_address_name'])
            place_url.append(place['place_url'])
            ID.append(place['id'])

        ar = np.array([ID,stores, X, Y, road_address,place_url]).T                          
        df = pd.DataFrame(ar, columns = ['ID','stores', 'X', 'Y','road_address','place_url']) 
        return df

    #사용자가 입력한 키워드로 정보를 받아와 데이터 프레임 생성
    def keywords(self):
        df = None
        for loca in self.locations:                                                         
            for page in range(1,4):                                                         
                local_name = self.elec_location(loca, page)                                
                local_elec_info = self.elec_info(local_name)                                

                if df is None:                                                              
                    df = local_elec_info
                elif local_elec_info is None:                                               
                    continue
                else:                                                                       
                    df = pd.concat([df, local_elec_info],join='outer', ignore_index = True)
        return df

    # 데이터 프레임을 기준으로 지도를 생성하는 함수
    def make_map(self,dfs):
        m = folium.Map(location=[37.5518911,126.9917937],                                   
                    zoom_start=7)

        minimap = MiniMap()                                                                 
        m.add_child(minimap)
        for i in range(len(dfs)):                                                           
            folium.Marker([dfs['Y'][i],dfs['X'][i]],                                       
                    tooltip=dfs['stores'][i],                                               
                    popup=dfs['place_url'][i],                                              
                    ).add_to(m)
        return m
    
    # 지도 기본설정
    def standard_map(self):
        m = folium.Map(location=[37.5518911,126.9917931],zoom_start=12)
        m.save('assets/map.html')

    # 키워드로 지도검색하는 함수
    async def map_search(self):
        if self.map_search_input == "":                                                          
            return rx.window_alert('Please enter your search term!')
        self.map_count+=1                        
        self.locations = self.map_search_input.split(',')                                         
        self.df = self.keywords()
        m = self.make_map(self.df)
        if self.map_html == '/map2.html':
            m.save('assets/map3.html')
            self.map_html = '/map3.html'
        else :
            m.save('assets/map2.html')
            self.map_html = '/map2.html'
        await asyncio.sleep(1)
        self.map_iframe = self.time_map_iframe
        self.df = self.df.drop_duplicates(['ID']) 
        self.df['place url'] = self.df['place_url']
        self.df = self.df.drop('place_url', axis=1)                                         
        self.df = self.df.reset_index()

    # 지도 초기화 함수
    def map_clear(self):
        self.map_html = '/map.html'

    # kakaoapi 길찾기함수
    async def get_directions(self):
        self.kakao_api()
        api_url = "https://apis-navi.kakaomobility.com/v1/directions"
        origin = self.start_location_x+','+self.start_location_y
        destination = self.end_location_x+','+self.end_location_y

        headers = {
            "Authorization": f"KakaoAK {self.KAKAO_REST_API_KEY}"
        }

        params = {
            "origin": origin,
            "destination": destination,
        }

        response = requests.get(api_url, headers=headers, params=params)
        result = response.json()
        m = folium.Map(location=[37.5518911,126.9917931],zoom_start=7)
        for i in range(0,len(result['routes'][0]['sections'][0]['guides'])):
            folium.Marker([result['routes'][0]['sections'][0]['guides'][i]['y'],result['routes'][0]['sections'][0]['guides'][i]['x']],                                       
                    tooltip=f'{i}번 위치',                                               
                    popup=f'{i}번 위치',                                              
                    ).add_to(m)
        if self.map_html == '/map2.html':
            m.save('assets/map3.html')
            self.map_html = '/map3.html'
        else :
            m.save('assets/map2.html')
            self.map_html = '/map2.html'
        await asyncio.sleep(1)
        self.map_iframe = self.time_map_iframe