Creative Code

Lunar-(6)홈화면 UI 구성 본문

Projects

Lunar-(6)홈화면 UI 구성

빛하루 2023. 12. 11. 00:54

홈화면(pages/home.py)

감기로 인해 3일만에 작업..

아래 메뉴에 보이는 추가 예정 기능들(홈화면, my profile, map, chat, Ai chat, diary, video, game, setting) --> 더 추가할 수도 있음..

 

왼쪽 Recommended friends에는 추후에 머신러닝을 이용해 사용자가 알 수도 있는 친구를 추천해주는 알고리즘을 만들어 적용시킬예정

 

Aurora프로젝트와 비슷하지만 게시글 업로드 시 각종 공감기능, 댓글기능 추가할 예정!

 

오늘 작업 코드 :

 

※pages/home.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.vstack(
                rx.container(
                    rx.hstack(
                        rx.image(src='/human.png',height='40px',width='40px'),
                        rx.text(
                            'Recommended freinds',
                            style={
                                'fontSize':'25px',
                                'fontWeight':'bolder',
                                'fontFamily':'Calibri, Calibri',
                                "background": "-webkit-linear-gradient(-45deg, #8ea6e6, #ad3ce6)",
                                '-webkit-background-clip':'text',
                                'color':'transparent',
                            },
                        ),
                    ),
                    rx.container(height='10px'),
                    rx.vstack(
                        rx.container(height='200px'),
                        border = '2px solid #000000',
                        border_radius='30px',
                    ),
                ),
                align_items='start',
            ),
            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.hstack(
            rx.image(src='/find2.png',height='35px',width='35px'),
            rx.input(
                on_change=HomeState.set_friend,
                placeholder="Search users",  # 사용자 검색을 위한 입력 상자
                width="100%",
                border = "3px solid #000000",
            ),
        ),
        rx.container(height='10px'),
        rx.foreach(
            HomeState.search_users,
            lambda user: rx.vstack(
                rx.hstack(
                    rx.avatar(name=user.username, size="sm"),  # 검색된 사용자의 아바타 이미지
                    rx.text(user.username),  # 검색된 사용자의 사용자 이름
                    rx.spacer(),
                    rx.button(
                        rx.icon(tag="add"),
                        on_click=lambda: HomeState.follow_user(user.username),  # 사용자를 팔로우하는 버튼
                    ),
                    width="100%",
                ),
                py=2,
                width="100%",
            ),
        ),
        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_search, placeholder="Search contents"),  # 콘텐츠 검색을 위한 입력 상자
        justify="space-between",
        p=4,
        border_bottom="3px solid #000000",
    )

# 새로운 게시물을 작성하는 컴포저
def composer(HomeState):
    """The composer for new tweets."""
    return rx.vstack(
        rx.container(height='5px'),
        rx.vstack(
            rx.hstack(
                rx.avatar(size="md"),  # 사용자의 아바타 이미지
                rx.container(width='30px'),
                rx.text_area(
                    value=HomeState.tweet,
                    w='100%',
                    border=2,
                    placeholder="What's happening?",  # 트윗을 작성하는 입력 상자
                    resize="none",
                    py=4,
                    px=0,
                    _focus={"border": 0, "outline": 0, "boxShadow": "none"},
                    on_change=HomeState.set_tweet,
                ),
                width='95%',
                margin_left = '30px',
            ),
            rx.hstack(
                rx.button(
                    "Select File",
                    border_radius="1em",
                    box_shadow="rgba(151, 65, 252, 0.8) 0 15px 30px -10px",
                    background_image="linear-gradient(144deg,#AF40FF,#5B42F3 50%,#00DDEB)",
                    box_sizing="border-box",
                    color="white",
                    opacity="0.6",
                    _hover={"opacity": 1},
                    on_click=HomeState.handle_file_selection,
                ),
                rx.button(
                    "Upload",
                    on_click= HomeState.post_tweet,
                    border_radius="1em",
                    box_shadow="rgba(151, 65, 252, 0.8) 0 15px 30px -10px",
                    background_image="linear-gradient(144deg,#AF40FF,#5B42F3 50%,#00DDEB)",
                    box_sizing="border-box",
                    color="white",
                    opacity="0.6",
                    _hover={"opacity": 1},
                    style={"margin-left": "auto"},  # Align to the right
                ),  # 트윗을 게시하는 버튼
                justify_content="flex-end",
                px=4,
                py=2,
                width='100%',
            ),
            rx.modal(
                rx.modal_overlay(
                    rx.modal_content(
                        rx.modal_header("File upload"),
                        rx.modal_body(
                            rx.responsive_grid(
                                rx.foreach(
                                    HomeState.img,
                                    lambda img: rx.vstack(
                                        rx.image(src=img),
                                        rx.text(img),
                                    ),
                                ),
                                columns=[2],
                                spacing="5px",
                            ),
                        ),
                        rx.modal_footer(
                            rx.button(
                                "Confirm", on_click=HomeState.change
                            ),
                            rx.button(
                                "Cancel",
                                on_click=HomeState.file_select_cancel,
                                border_radius="1em",
                                box_shadow="rgba(151, 65, 252, 0.8) 0 15px 30px -10px",
                                background_image="linear-gradient(144deg,#AF40FF,#5B42F3 50%,#00DDEB)",
                                box_sizing="border-box",
                                color="white",
                                opacity="0.6",
                                _hover={"opacity": 1},
                            ),
                        ),
                    )
                ),
                is_open=HomeState.imgshow,
            ),
            rx.responsive_grid(
                rx.foreach(
                    HomeState.img,
                    lambda img: rx.vstack(
                        rx.text(img),
                    ),
                ),
                columns=[2],
                spacing="5px",
            ),
            margin_left='5px',
            width='97%',
            border_radius='20px',
            border="3px solid #000000",
        ),
    )


# 개별 트윗을 표시하는 함수
def tweet(tweet):
    image_tags = rx.cond(
        tweet.image_content,
        rx.foreach(
            tweet.image_content.split(", "),
            lambda image: rx.image(src=f"/{image}", alt="tweet image")
        ),
        rx.box()  # 이미지가 없는 경우 빈 리스트를 반환합니다.
    ),

    return rx.vstack(
        rx.hstack(
            rx.container(width='5px'),
            rx.vstack(
                rx.avatar(name=tweet.author, size="sm"),  # 트윗 작성자의 아바타 이미지
            ),
            rx.box(
                rx.hstack(
                    rx.text("@" + tweet.author, font_weight="bold"),  # 트윗 작성자의 사용자 이름
                    rx.text("["+ tweet.created_at +"]"),
                ),
                rx.text(tweet.content, width="100%"),  # 트윗 내용
                *image_tags,
                width = '100%',
            ),
            py=4,
            gap=1,
            border="3px solid #3498db",
            border_radius='10px',
            width='98%',
        ),
        rx.container(height='5px'),
        margin_left='15px',
        align_items='start',
        width='auto',
    )

# 피드 영역
def feed(HomeState):
    """The feed."""
    return rx.box(
        feed_header(HomeState),
        composer(HomeState),
        rx.container(height='10px'),
        rx.cond(
            HomeState.tweets,
            rx.foreach(
                HomeState.tweets,
                tweet
            ),
            rx.vstack(
                rx.button(
                    rx.icon(
                        tag="repeat",
                        mr=1,
                    ),
                    rx.text("Click to load story"),
                    on_click=HomeState.get_tweets,
                ),  # 트윗을 불러오는 버튼
                p=4,
            ),
        ),
        h="100%",
    )

# 홈 페이지
def home():
    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, Tweet, User
from tkinter import filedialog


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

    tweet: str
    tweets: list[Tweet] = []

    friend: str
    search: str

    img: list[str]                                                             
    files: list[str] = []
    imgshow:bool=False

    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)

    #story 파일 선택 취소 함수        
    async def file_select_cancel(self):
        self.img=[]
        self.files=[]
        self.change()
    

    #게시물 업로드 함수
    async def post_tweet(self):
        if not self.logged_in:
            return rx.window_alert("Please log in to post a tweet.")                 # 로그인이 되어있지 않을 시 경고 메시지
        if len(self.tweet)==0:
            return rx.window_alert('Please write at least one character!')           # story 추가시 최소 한 글자 입력 경고 메시지
        if len(self.tweet)>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 모델 저장
            tweet = Tweet(
                author=self.user.username,                                           # author : 유저 아이디
                content=self.tweet,                                                  # content : 유저 story 입력 내용
                created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),             # created_at : stroy 작성 시간
                image_content=", ".join(self.files),                                 # image_content : 이미지 파일
            )
            
            session.add(tweet)
            session.commit()
            self.tweet = ""                                                          # session에 저장 후 story내용 초기화
            self.img=[]
            self.files=[]
            
        return self.get_tweets()

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

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

    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 []