티스토리 뷰
1. 데이터베이스와 API
1) 데이터베이스 테이블 구조
[elsword]
엘소드의 캐릭터 정보를 담고 있는 테이블입니다.
[quest]
엘소드 퀘스트 정보를 담고 있는 테이블입니다.
[quest_progress]
엘소드 퀘스트를 진행 중인 캐릭터 정보를 담고 있는 테이블입니다.
2) API
1. 엘소드 캐릭터 정보 등록 apI
@app.post("/insert/elsword")
async def insert_elsword(item: ElswordItem):
elsword = create_elsword(item)
session.add(elsword)
session.commit()
return f"{item.name} 추가 완료"
이 api는 앱에서 사용하는 것이 아닌 단순히 데이터베이스를 채우기 위해 사용한 api입니다.
2. 퀘스트 등록 api
@app.post("/insert/elsword/quest")
async def insert_elsword_quest(item: QuestItem):
quest = create_quest(item)
history = session.query(Quest).filter(Quest.name == item.name).first()
if history is None:
session.add(quest)
session.commit()
result = f"{item.name}를 등록하였습니다."
else:
result = f"{item.name}은 이미 등록된 퀘스트입니다."
return result
엘소드 퀘스트를 등록하는 api입니다.
등록하기 전에 중복 데이터를 방지하기 위해서 검사를 진행합니다.
history 변수에서 중복 조건인 이름이 동일한 것이 있는지 확인합니다.
if 문을 이용하여 None 여부를 체크하고 없을 때에만 데이터를 추가합니다.
이번 api에서는 중복 데이터가 들어와도 정상 처리를 하고 메시지만 전송합니다.
3. 퀘스트 리스트 조회 api
@app.get("/select/elsword/quest")
async def read_elsword_quest():
session.commit()
quest = session.query(Quest).all()
return [
{
"progress": calculate_task_progress(item.complete),
"id": item.id,
"name": item.name
}
for item in quest
]
def calculate_task_progress(complete: str):
list = complete.split(",")
length = len(list) if list[0] != "" or list[-1] != "" else 0
print(length)
return (length / 56) * 100
전체 퀘스트를 조회한 뒤 반복문을 돌리며 데이터를 가공하여 리스트를 리턴합니다.
4. 퀘스트 삭제 api
@app.delete("/delete/elsword/quest")
async def delete_elsword_quest(id: int):
session.execute(delete(Quest).where(Quest.id == id))
session.commit()
return "삭제 완료"
id를 받아서 퀘스트를 삭제합니다.
5. 퀘스트 상세 정보 조회 api
from sqlalchemy.orm import aliased, load_only
@app.get("/select/elsword/quest/detail")
async def select_elsword_quest_detail():
session.commit()
quest = session.query(Quest).all()
allowed_fields = ["characterGroup", "name", "questImage"]
elsword = session.query(Elsword).filter(Elsword.classType == "master").options(load_only(*allowed_fields)).all()
return [
{
"id": item.id,
"name": item.name,
"progress": calculate_task_progress(item.complete),
"character": [
{
"name": char.name,
"image": char.questImage,
"group": char.characterGroup,
"isComplete": char.name in item.complete,
"isOngoing": char.name in item.ongoing
}
for char in elsword
]
}
for item in quest
]
load_only를 이용해서 조회 시 특정 칼럼의 정보만 가져올 수 있습니다.
quest 테이블의 정보와 elsword 테이블의 정보를 결과 데이터 형식에 맞게 조합을 해서 리턴합니다.
6. 퀘스트 상태 업데이트 api
@app.post("/update/elsword/quest")
async def update_elsword_quest(item: QuestUpdateItem):
quest = session.query(Quest).filter(Quest.id == item.id).first()
if item.type == "complete":
quest.complete = add_name_to_text(quest.complete, item.name)
quest.ongoing = remove_name_to_text(quest.ongoing, item.name)
await delete_quest_progress(item.id, item.name)
elif item.type == "ongoing":
quest.ongoing = add_name_to_text(quest.ongoing, item.name)
quest.complete = remove_name_to_text(quest.complete, item.name)
await create_quest_progress(item.id, item.name)
elif item.type == "remove":
quest.complete = remove_name_to_text(quest.complete, item.name)
quest.ongoing = remove_name_to_text(quest.ongoing, item.name)
await delete_quest_progress(item.id, item.name)
session.commit()
return "업데이트 완료"
def add_name_to_text(text, name):
if text:
result = f"{text},{name}"
else:
result = name
return result
def remove_name_to_text(text, name):
result = text.replace(f"{name},", "").replace(f",{name}", "").replace(name, "")
if result.endswith(","):
result = result[:-1]
return result
async def create_quest_progress(id, name):
progress = create_init_quest_progress(id, name)
session.add(progress)
return f"{name} 추가 완료"
async def delete_quest_progress(id, name):
session.query(QuestProgress).filter(QuestProgress.quest_id == id, QuestProgress.name == name).delete(synchronize_session=False)
session.commit()
return f"{name} 삭제 완료"
이번 api는 조금 지저분한 api입니다.
이 부분이 가장 고민을 많이 했던 부분입니다. 아직 이런 쪽은 경험이 없어서 더 효율적인 방법은 아직 잘 모르겠습니다.
엘소드에 전직 종류가 총 56가지인데 퀘스트를 하나 만들 때마다 56개의 전직의 퀘스트 진행 상태의 값이 필요로 합니다.
퀘스트가 추가/삭제될 때마다 56개의 데이터가 추가/삭제가 되도록 하면 위의 지저분한 코드가 많이 줄어들겠지만 데이터베이스가 지저분해질 것 같습니다.
그래서 진행 중인 것만 테이블에서 따로 저장해 두고 진행 중, 완료 상태는 문자열 형태로 저장하기로 하였습니다.
위에 코드는 상태 값에 따라 quest_progress를 추가/삭제하고 quest의 ongoing/complete 정보를 변경하는 코드입니다.
7. 퀘스트 진행 조회
@app.get("/select/elsword/counter")
async def read_elsword_quest_progress():
session.commit()
sql = """
SELECT quest_progress.id, quest_progress.name, quest_progress.quest_id, quest_progress.progress, quest.max, elsword.questImage, elsword.characterGroup
FROM quest, quest_progress, elsword
WHERE quest_progress.quest_id = quest.id AND elsword.name = quest_progress.name and elsword.classType = 'master'
"""
result = session.execute(text(sql)).fetchall()
formatted_result = [
{
"id": row[0],
"name": row[1],
"quest_id": row[2],
"progress": row[3],
"max": row[4],
"image": row[5],
"characterGroup": row[6],
}
for row in result
]
return formatted_result
이번에는 SQL을 이용해서 조회를 해보았습니다.
SQL보다 sqlalchemy가 좀 더 이해하기 난해해서 SQL로 하였습니다.
8. 퀘스트 진행 업데이트
@app.post("/update/elsword/counter")
async def update_elsword_quest_progress(item: QuestProgressUpdateItem):
"""
퀘스트 상태 업데이트
- **id**: 퀘스트 진행 id
- **max**: 퀘스트 개수
"""
progressItem = session.query(QuestProgress).filter(QuestProgress.id == item.id).first()
progressItem.progress += 1
session.commit()
if progressItem.progress >= item.max:
await update_elsword_quest(QuestUpdateItem(id=progressItem.quest_id, name=progressItem.name, type="complete"))
return 0
return progressItem.progress
진행도를 업데이트한 뒤 진행도가 퀘스트 개수랑 동일하거나 크면 완료 상태로 업데이트합니다.
2. UI
0) 게임 화면
시작하기 전 메인 탭 중 하나인 게임 화면입니다.
이 화면에서는 다른 화면으로 이동하기 위한 카드들을 제공합니다.
카드 안에 글씨를 보면 텍스트에 아웃라인이 있습니다.
폰트를 사용할 수도 있지만 코드로 하는 방법을 찾아보았습니다.
@Composable
fun OutlineText(
text: String,
style: TextStyle,
outlineColor: Color,
modifier: Modifier = Modifier
) {
Box(modifier = modifier) {
Text(
text = text,
style = style.merge(
TextStyle(
color = outlineColor,
fontSize = style.fontSize,
drawStyle = Stroke(width = 10f, join = StrokeJoin.Round)
)
)
)
Text(text = text, style = style)
}
}
첫 번째 텍스트는 아웃라인을 나타냅니다.
두 번째 텍스트는 내부의 텍스트를 나타냅니다.
이렇게 하면 아웃라인이 있는 텍스트를 만들 수 있습니다.
만약 아웃라인의 두께를 바꾸고 싶다면 drawStyle → Stroke의 width의 값을 변경해 주면 됩니다.
1) 엘소드 캐릭터 소개 화면 (직업 선택 화면)
엘소드의 캐릭터 별 직업 선택을 할 수 있는 화면입니다.
< > 버튼이나 하단의 캐릭터의 이름을 선택하여 캐릭터를 변경할 수 있습니다.
선택된 캐릭터 단위로 4개의 전직이 보이게 되며 1개의 전직을 선택하여 상세 화면으로 넘어갑니다.
지금은 단순히 이미지 전환 형식으로 되어있는데 뷰페이저를 사용하는 것도 고려를 해봐야겠습니다.
실제 화면에서의 비율입니다.
Column의 가중치를 주어 비율을 지정하였습니다.
해당 페이지의 이미지들은 로딩이 있으면 어색할 것 같아서 리소스 파일에 저장한 뒤 이용하였습니다.
enum class ElswordCharacters(
val characterName: String,
val color: Color,
val sdImage: Int,
val jobImage: List<Int>
) {
Elsword(
"엘소드",
Color(0xFFBA333D),
R.drawable.img_elsword_sd,
listOf(
R.drawable.img_elsword_sd_1,
R.drawable.img_elsword_sd_2,
R.drawable.img_elsword_sd_3,
R.drawable.img_elsword_sd_4
)
),
... (생략) ...
}
화면에 들어갈 정보들을 enum class를 활용하여 데이터를 만들어주었습니다.
private val characterList = ElswordCharacters.values()
private val _selectCharacter = mutableStateOf(0)
val selectCharacter: State<Int> = _selectCharacter
val currentCharacter: ElswordCharacters
get() = characterList[_selectCharacter.value]
enum class로 만들었기 때문에 values()를 이용하여 리스트를 가져올 수 있습니다.
그리고 선택된 값을 저장할 _selectCharacter 변수와 현재 선택된 캐릭터의 값을 전달해 줄 currentCharacter를 활용하여 화면을 구현하였습니다.
2) 엘소드 캐릭터 소개 상세 화면
엘소드 캐릭터 소개 상세 화면입니다.
공식 홈페이지의 있는 디자인을 앱에 맞게 디자인을 한 화면입니다.
단순 정보성 페이지라 특별한 기능은 없습니다.
현재 데이터베이스 내용 채우는 것 관련해서 고민 중이어서 아직 작업은 하지 않았습니다.
3) 엘소드 카운터
등록한 퀘스트의 진행사항을 표시해 주는 화면입니다.
- 버튼을 이용하여 퀘스트를 등록합니다.
등록된 퀘스트 별로 서버에서 조회를 통해 진행 상황을 표시합니다.
진행 상태는 [미진행, 진행 중, 진행 완료] 이렇게 3가지 형태가 있으며 위의 이미지와 같이 표시합니다.
캐릭터를 선택하여 진행 상태를 변경할 수 있습니다.
상태 업데이트 다이얼로그 디자인을 게임스럽게 하고 싶은데 아직 디자인을 결정 못 해서 임시로 색상으로만 구분하였습니다.
퀘스트 상태에 따라 UI를 다르게 해야 하는 것이 이번 화면에서 가장 중요한 부분입니다.
AsyncImage(
model = character.image,
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxSize()
)
우선 이미지를 서버에서 가져와야 하는데 저는 Coil 라이브러리를 사용하였습니다.
사용 방법은 크게 어렵지 않으니 넘어갑니다.
AsyncImage(
model = character.image,
contentDescription = null,
contentScale = ContentScale.FillWidth,
colorFilter = ColorFilter.colorMatrix(
ColorMatrix().apply {
setToSaturation(if (character.isComplete) 1f else 0f)
}
),
modifier = Modifier.fillMaxSize()
)
위의 코드에서 colorFilter를 추가하였습니다.
ColorMatrix의 setToSaturation를 이용하여 채도 값을 변경합니다.
1f가 기본 채도이고 0f로 설정하면 회색으로 표시됩니다.
이렇게 해서 완료 상태와 그렇지 않았을 때의 색상을 조정하였습니다.
다음은 진행 중일때 표시입니다.
val infiniteTransition = rememberInfiniteTransition()
val imageAlpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0.5f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
},
repeatMode = RepeatMode.Reverse
)
)
val textAlpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
},
repeatMode = RepeatMode.Reverse
)
)
...(중략)...
if (character.isOngoing) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(color.copy(alpha = imageAlpha))
) {
Text(
text = "진행중",
style = textStyle24B().copy(color = MyColorWhite.copy(alpha = textAlpha))
)
}
}
단순하게 배경에 알파값만 주어서 표시할 수 있긴 하지만
진행 중인 것을 강조하기 위해 애니메이션을 주었습니다.
rememberInfiniteTransition()를 이용하여 무한 반복하는 애니메이션을 만들 수 있습니다.
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0.5f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
},
repeatMode = RepeatMode.Reverse
)
)
알파 값은 Float 형태로 지정하기 때문에 animatFloat를 사용합니다.
초깃값과 최종 값, 그리고 애니메이션 방식을 지정합니다.
RepeatMode.Restart를 하게 되면 inital → target / inital → target으로 진행되며 부자연스럽게 깜빡이는 느낌이 되므로 RepeatMode.Reverse를 이용해서 inital → target → inital 형태로 동작하게 합니다.
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(color.copy(alpha = imageAlpha))
) {
Text(
text = "진행중",
style = textStyle24B().copy(color = MyColorWhite.copy(alpha = textAlpha))
)
}
그다음 원하는 곳에 alpha 값을 지정해 줍니다.
저의 경우 텍스트와 배경의 target 값이 달라서 2개를 만들었습니다.
inital과 target 값이 같다면 하나만 만들어도 됩니다.
4) 카운터 등록 및 삭제 화면
카운터 등록 및 삭제 화면입니다.
상단에 퀘스트를 등록하는 카드가 고정으로 설정되어 있습니다.
하단에 리스트로 등록된 퀘스트와 진행도를 표시하며 x 버튼을 통해 퀘스트를 삭제할 수 있습니다.
이 부분은 특별한 부분이 없어서 코드는 생략합니다.
'안드로이드 > 코드' 카테고리의 다른 글
Mj App : 달력 (0) | 2023.06.27 |
---|---|
Mj App : 게임 - 포켓몬 (0) | 2023.06.27 |
Mj App : 공통 (0) | 2023.06.27 |
Compose 내 관리앱 만들기 : 3. 교통 - 지하철 (0) | 2023.03.19 |
Compose 내 관리앱 만들기 : 2. 교통 - 버스 (0) | 2023.03.19 |
- Total
- Today
- Yesterday
- 웹뷰
- 안드로이드 구글 지도
- 안드로이드
- Retrofit
- Compose BottomSheetScaffold
- Gradient
- Fast api
- Compose 네이버 지도 api
- WebView
- Compose BottomSheetDialog
- Row
- WorkManager
- Duplicate class fond 에러
- LazyColumn
- 포켓몬 도감
- Android
- Pokedex
- Duplicate class found error
- Android Compose
- Compose BottomSheet
- Compose MotionLayout
- Compose Naver Map
- Compose ConstraintLayout
- Worker
- column
- compose
- Kotlin
- Compose QRCode Scanner
- Compose 네이버 지도
- Compose ModalBottomSheetLayout
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |